-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbootstrap.sh
More file actions
executable file
·116 lines (102 loc) · 4.33 KB
/
bootstrap.sh
File metadata and controls
executable file
·116 lines (102 loc) · 4.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env bash
#
# alpine-self-setup: build an Alpine Linux sysroot tarball from a JSON config.
#
# Usage: ./bootstrap.sh <output.tar> <config.json>
# Example: ./bootstrap.sh ./alpine.tar example.json
#
# The config JSON looks like:
# {
# "arch": "x86_64",
# "repos": ["https://dl-cdn.alpinelinux.org/alpine/v3.21/main",
# "https://dl-cdn.alpinelinux.org/alpine/v3.21/community"],
# "packages": ["bash", "clang", "openrc"]
# }
#
# Why a tar (and not a directory) is the output:
# apk.static runs under `unshare --user --map-root-user`, where we appear as
# UID 0 *inside* the namespace. apk does chowns to non-root system users
# (mail, daemon, etc.); from the host's perspective those map to the
# overflow UID (65534), so a directory left behind has wrong ownership.
# Tarring inside the same namespace records the in-namespace UIDs/GIDs
# correctly, and the archive can later be extracted by real root with
# proper attributes preserved.
#
# Requires on the host: bash, curl, jq, tar, unshare (util-linux), coreutils.
# Linux only.
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <output.tar> <config.json>" >&2
exit 2
fi
OUTPUT="$1"
CONFIG="$2"
for tool in curl jq tar unshare; do
command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 1; }
done
[ -f "$CONFIG" ] || { echo "config not found: $CONFIG" >&2; exit 1; }
# Normalize OUTPUT to an absolute path so it survives any cwd shifts.
case "$OUTPUT" in
/*) ;;
*) OUTPUT="$PWD/$OUTPUT" ;;
esac
echo ">> reading config: $CONFIG"
ARCH=$(jq -r '.arch' "$CONFIG")
mapfile -t PACKAGES < <(jq -r '.packages[]' "$CONFIG")
mapfile -t REPOS < <(jq -r '.repos[]' "$CONFIG")
echo " arch: $ARCH"
echo " repos: ${#REPOS[@]}"
for r in "${REPOS[@]}"; do echo " - $r"; done
echo " packages: ${#PACKAGES[@]}"
for p in "${PACKAGES[@]}"; do echo " - $p"; done
echo " output: $OUTPUT"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
echo ">> work dir: $WORK"
# Discover the latest apk-tools-static. Alpine prunes old point releases from
# the mirror, so we scrape the directory listing instead of pinning a URL.
# `latest-stable` is an Alpine-maintained symlink to the current stable branch.
LISTING="https://dl-cdn.alpinelinux.org/alpine/latest-stable/main/x86_64/"
echo ">> resolving apk-tools-static"
echo " listing: $LISTING"
APK_FILE=$(curl -fsSL "$LISTING" | grep -oE 'apk-tools-static-[^"]*\.apk' | head -1)
[ -n "$APK_FILE" ] || { echo "could not find apk-tools-static at $LISTING" >&2; exit 1; }
echo " found: $APK_FILE"
APK_URL="${LISTING}${APK_FILE}"
APK_DEST="$WORK/apk-tools.apk"
echo ">> downloading apk-tools-static"
echo " from: $APK_URL"
echo " to: $APK_DEST"
curl -fsSL "$APK_URL" -o "$APK_DEST"
echo " size: $(stat -c%s "$APK_DEST") bytes"
# An apk file is a tarball with a defined structure (concatenated gzip
# streams); sbin/apk.static is in the data segment.
echo ">> extracting sbin/apk.static from the apk"
tar -xzf "$APK_DEST" -C "$WORK" sbin/apk.static
APK_STATIC="$WORK/sbin/apk.static"
chmod +x "$APK_STATIC"
echo " apk.static: $APK_STATIC ($(stat -c%s "$APK_STATIC") bytes)"
# Minimal sysroot skeleton apk.static expects to populate.
SYSROOT="$WORK/sysroot"
echo ">> preparing sysroot skeleton at $SYSROOT"
mkdir -p "$SYSROOT"/{dev,etc/apk/keys,lib/apk/db,proc,run,sys,tmp,var/cache/apk}
: > "$SYSROOT/etc/apk/world"
# Build a helper script that runs apk add AND tar in the same user namespace.
# Each argument is %q-quoted so values with spaces/special chars survive.
HELPER="$WORK/install-and-pack.sh"
echo ">> generating helper script: $HELPER"
{
printf '#!/bin/bash\nset -euo pipefail\n'
printf '%q add --root %q --arch %q --allow-untrusted --initdb' \
"$APK_STATIC" "$SYSROOT" "$ARCH"
for r in "${REPOS[@]}"; do printf ' --repository %q' "$r"; done
for p in "${PACKAGES[@]}"; do printf ' %q' "$p"; done
printf '\n'
printf 'tar --numeric-owner -cf %q -C %q .\n' "$OUTPUT" "$SYSROOT"
} > "$HELPER"
chmod +x "$HELPER"
echo ">> entering user namespace (unshare --user --map-root-user) to:"
echo " 1. apk.static add (install ${#PACKAGES[@]} package(s) into $SYSROOT)"
echo " 2. tar (pack $SYSROOT -> $OUTPUT)"
unshare --user --map-root-user -- "$HELPER"
echo ">> done. tarball: $OUTPUT ($(stat -c%s "$OUTPUT") bytes)"