Skip to content

Commit ab0689c

Browse files
authored
batch of fixes and enhancements - early january'2025 (#257)
> further enhancements: > - can now build (and develop) Hook for any arch under any arch, including for amd64 under Darwin arm64 > - full support for building on macOS+brew, including on arm64 > - added `shellfmt` tool and bash code format enforcing on CI (in addition to shellcheck) > - avoid pulling skopeo image over and over again ---- ### build: implement `shellfmt` (and `lint` which does both shellcheck/fmt) - for consistent bash formatting - include an .editorconfig for IDE's ### gha: switch to `lint` (which does both `shellcheck` and `shellfmt`) ### linuxkit: bump 1.5.2 -> 1.5.3 ### build: implement build-host dependency handling for macOS+brew - if on macOS+brew: - detects missing deps and installs them with brew - exports PATH with brew-based GNU versions first - coreutils, gnu-sed and gnu-tar included ### build: Dockerfile: fix FROM xx AS casing - to squash recent BuildKit warnings ### build: pass `--verbose 2` to linuxkit if `DEBUG=yes` - can help with some edge cases ### build: refactor skopeo pull and list-tags functions - this only affects Armbian kernel flavours - avoids pulling if found in local cache ### build: use skopeo `v1.17.0` instead of latest - since we now use the local tag ### build: armbian: kernel: refactor Dockerfile with helper - building the Armbian kernels would produce different hashes depending on the arch of the host - moving the affected code into the Dockerfile would lead to escaping pain; instead implement a docker.sh helper - in practice, all code in the Dockerfile is hashed, but the arch decision is now therein and hash won't change - also, allows for reuse, which is bound to come later ### build: docker: detect & export `DOCKER_HOST` from current `docker context` - Some Docker-in-VM solutions (like Docker Desktop, colima, etc) set a non-default docker context pointing to the correct socket - Seems LinuxKit fumbles detecting this and ends up silently failing all local-Docker-daemon cache hits - that is fine for CI, where all images are (beforehand) pushed to the registry (and thus LK ends up pulling from remote), but not during local development - reported to upstream LinuxKit: linuxkit/linuxkit#4092 ### build: kernel: force target arch on cross-built kernel docker image manifest - our kernel builds are done in arch-independent Dockerfiles - but those get the build-host's architecture, despite the contents being correct - when locally developing on a kernel that is != host-arch - those get the host-arch in the image - but LinuxKit refuses to use it due to arch mismatch - (when pushed to a registry, the arch info is discarded, and LK is ok with that) - thus - introduce `ensure_docker_image_architecture(imagetag, arch)` - which just hacks at manifests via a docker save/docker load - call it from both default and armbian kernel builds ### build: docker: avoid Docker Inc's "What's next" hints - enough spam already, thanks ----- Signed-off-by: Ricardo Pardini <[email protected]>
2 parents 9ba394a + 4356a41 commit ab0689c

File tree

15 files changed

+366
-87
lines changed

15 files changed

+366
-87
lines changed

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[*]
2+
charset = utf-8
3+
end_of_line = lf
4+
indent_style = tab
5+
indent_size = 4
6+
trim_trailing_whitespace = true
7+
insert_final_newline = true
8+
9+
[*.sh]
10+
shell_variant = bash
11+
binary_next_line = false
12+
switch_case_indent = true
13+
ij_shell_switch_cases_indented = true
14+
space_redirects = true
15+
keep_padding = false
16+
function_next_line = false

.github/workflows/build-all-matrix.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,8 @@ jobs:
5151
id: date_prep
5252
run: echo "created=$(date -u +'%Y%m%d-%H%M')" >> "${GITHUB_OUTPUT}"
5353

54-
- name: Run shellcheck # so fail fast in case of bash errors/warnings
55-
id: shellcheck
56-
run: bash build.sh shellcheck
54+
- name: Run lint (shellcheck/shellfmt) # so fail fast in case of bash errors/warnings or unformatted code
55+
run: bash build.sh lint
5756

5857
- name: Run the matrix JSON preparation bash script
5958
id: prepare-matrix

bash/common.sh

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,59 @@ function log() {
2222

2323
function install_dependencies() {
2424
declare -a debian_pkgs=()
25-
[[ ! -f /usr/bin/jq ]] && debian_pkgs+=("jq")
26-
[[ ! -f /usr/bin/envsubst ]] && debian_pkgs+=("gettext-base")
27-
[[ ! -f /usr/bin/pigz ]] && debian_pkgs+=("pigz")
28-
29-
# If running on Debian or Ubuntu...
30-
if [[ -f /etc/debian_version ]]; then
31-
# If more than zero entries in the array, install
32-
if [[ ${#debian_pkgs[@]} -gt 0 ]]; then
33-
log warn "Installing dependencies: ${debian_pkgs[*]}"
25+
declare -a brew_pkgs=()
26+
27+
command -v jq > /dev/null || {
28+
debian_pkgs+=("jq")
29+
brew_pkgs+=("jq")
30+
}
31+
32+
command -v pigz > /dev/null || {
33+
debian_pkgs+=("pigz")
34+
brew_pkgs+=("pigz")
35+
}
36+
37+
command -v envsubst > /dev/null || {
38+
debian_pkgs+=("gettext-base")
39+
brew_pkgs+=("gettext")
40+
}
41+
42+
if [[ "$(uname)" == "Darwin" ]]; then
43+
command -v gtar > /dev/null || brew_pkgs+=("gnu-tar")
44+
command -v greadlink > /dev/null || brew_pkgs+=("coreutils")
45+
command -v gsed > /dev/null || brew_pkgs+=("gnu-sed")
46+
fi
47+
48+
# If more than zero entries in the array, install
49+
if [[ ${#debian_pkgs[@]} -gt 0 ]]; then
50+
# If running on Debian or Ubuntu...
51+
if [[ -f /etc/debian_version ]]; then
52+
log warn "Installing apt dependencies: ${debian_pkgs[*]}"
3453
sudo DEBIAN_FRONTEND=noninteractive apt -o "Dpkg::Use-Pty=0" -y update
3554
sudo DEBIAN_FRONTEND=noninteractive apt -o "Dpkg::Use-Pty=0" -y install "${debian_pkgs[@]}"
55+
elif [[ "$(uname)" == "Darwin" ]]; then
56+
log info "Skipping Debian deps installation for Darwin..."
57+
else
58+
log error "Don't know how to install the equivalent of Debian packages *on the host*: ${debian_pkgs[*]} -- teach me!"
3659
fi
3760
else
38-
log error "Don't know how to install the equivalent of Debian packages: ${debian_pkgs[*]} -- teach me!"
61+
log info "All deps found, no apt installs necessary on host."
62+
fi
63+
64+
if [[ "$(uname)" == "Darwin" ]]; then
65+
if [[ ${#brew_pkgs[@]} -gt 0 ]]; then
66+
log info "Detected Darwin, assuming 'brew' is available: running 'brew install ${brew_pkgs[*]}'"
67+
brew install "${brew_pkgs[@]}"
68+
fi
69+
70+
# Re-export PATH with the gnu-version of coreutils, tar, and sed
71+
declare brew_prefix
72+
brew_prefix="$(brew --prefix)"
73+
export PATH="${brew_prefix}/opt/gnu-sed/libexec/gnubin:${brew_prefix}/opt/gnu-tar/libexec/gnubin:${brew_prefix}/opt/coreutils/libexec/gnubin:${PATH}"
74+
log debug "Darwin; PATH is now: ${PATH}"
3975
fi
4076

41-
return 0 # there's a shortcircuit above
77+
return 0
4278
}
4379

4480
# utility used by inventory.sh to define a kernel/flavour with less-terrible syntax.

bash/docker.sh

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
function check_docker_daemon_for_sanity() {
2+
# LinuxKit is a bit confused about `docker context list` when you're using a non-default context.
3+
# Let's obtain the currect socket from the current context and explicitly export it.
4+
# This allows working on machines with Docker Desktop, colima, and other run-linux-in-a-VM solutions.
5+
declare current_context_docker_socket="" current_docker_context_name=""
6+
current_docker_context_name="$(docker context show)"
7+
current_context_docker_socket="$(docker context inspect "${current_docker_context_name}" | jq -r '.[0].Endpoints.docker.Host')"
8+
log info "Current Docker context ('${current_docker_context_name}') socket: '${current_context_docker_socket}'"
9+
10+
log debug "Setting DOCKER_HOST to '${current_context_docker_socket}'"
11+
export DOCKER_HOST="${current_context_docker_socket}"
12+
13+
# Hide Docker, Inc spamming "What's next" et al.
14+
export DOCKER_CLI_HINTS=false
15+
216
# Shenanigans to go around error control & capture output in the same effort, 'docker info' is slow.
317
declare docker_info docker_buildx_version
418
docker_info="$({ docker info 2> /dev/null && echo "DOCKER_INFO_OK"; } || true)"
@@ -23,3 +37,159 @@ function check_docker_daemon_for_sanity() {
2337
}
2438

2539
}
40+
41+
# Utility to pull skopeo itself from SKOPEO_IMAGE; checks the local Docker cache and skips if found
42+
function pull_skopeo_image_if_not_in_local_docker_cache() {
43+
# Check if the image is already in the local Docker cache
44+
if docker image inspect "${SKOPEO_IMAGE}" &> /dev/null; then
45+
log info "Skopeo image ${SKOPEO_IMAGE} is already in the local Docker cache; skipping pull."
46+
return 0
47+
fi
48+
49+
log info "Pulling Skopeo image ${SKOPEO_IMAGE}..."
50+
51+
pull_docker_image_from_remote_with_retries "${SKOPEO_IMAGE}"
52+
}
53+
54+
# Utility to get the most recent tag for a given image, using Skopeo. no retries, a failure is fatal.
55+
# Sets the value of outer-scope variable latest_tag_for_docker_image, so declare it there.
56+
# If extra arguments are present after the image, they are used to grep the tags.
57+
function get_latest_tag_for_docker_image_using_skopeo() {
58+
declare image="$1"
59+
shift
60+
latest_tag_for_docker_image="undetermined_tag"
61+
62+
# Pull separately to avoid tty hell in the subshell below
63+
pull_skopeo_image_if_not_in_local_docker_cache
64+
65+
# if extra arguments are present, use them to grep the tags
66+
if [[ -n "$*" ]]; then
67+
latest_tag_for_docker_image="$(docker run "${SKOPEO_IMAGE}" list-tags "docker://${image}" | jq -r ".Tags[]" | grep "${@}" | tail -1)"
68+
else
69+
latest_tag_for_docker_image="$(docker run "${SKOPEO_IMAGE}" list-tags "docker://${image}" | jq -r ".Tags[]" | tail -1)"
70+
fi
71+
log info "Found latest tag: '${latest_tag_for_docker_image}' for image '${image}'"
72+
}
73+
74+
# Utility to pull from remote, with retries.
75+
function pull_docker_image_from_remote_with_retries() {
76+
declare image="$1"
77+
declare -i retries=3
78+
declare -i retry_delay=5
79+
declare -i retry_count=0
80+
81+
while [[ ${retry_count} -lt ${retries} ]]; do
82+
if docker pull "${image}"; then
83+
log info "Successfully pulled ${image}"
84+
return 0
85+
else
86+
log warn "Failed to pull ${image}; retrying in ${retry_delay} seconds..."
87+
sleep "${retry_delay}"
88+
((retry_count += 1))
89+
fi
90+
done
91+
92+
log error "Failed to pull ${image} after ${retries} retries."
93+
exit 1
94+
}
95+
96+
# Helper script, for common task of installing packages on a Debian Dockerfile
97+
# always includes curl and downloads ORAS binary too
98+
# takes the relative directory to write the helper to
99+
# sets outer scope dockerfile_helper_filename with the name of the file for the Dockerfile (does not include the directory)
100+
function produce_dockerfile_helper_apt_oras() {
101+
declare target_dir="$1"
102+
declare helper_name="apt-oras-helper.sh"
103+
dockerfile_helper_filename="Dockerfile.autogen.helper.${helper_name}" # this is negated in .dockerignore
104+
105+
declare fn="${target_dir}${dockerfile_helper_filename}"
106+
cat <<- 'DOWNLOAD_HELPER_SCRIPT' > "${fn}"
107+
#!/bin/bash
108+
set -e
109+
declare oras_version="1.2.2" # See https://github.com/oras-project/oras/releases
110+
# determine the arch to download from current arch
111+
declare oras_arch="unknown"
112+
case "$(uname -m)" in
113+
"x86_64") oras_arch="amd64" ;;
114+
"aarch64" | "arm64") oras_arch="arm64" ;;
115+
*) log error "ERROR: ARCH $(uname -m) not supported by ORAS? check https://github.com/oras-project/oras/releases" && exit 1 ;;
116+
esac
117+
declare oras_down_url="https://github.com/oras-project/oras/releases/download/v${oras_version}/oras_${oras_version}_linux_${oras_arch}.tar.gz"
118+
export DEBIAN_FRONTEND=noninteractive
119+
apt-get -qq -o "Dpkg::Use-Pty=0" update || apt-get -o "Dpkg::Use-Pty=0" update
120+
apt-get -qq install -o "Dpkg::Use-Pty=0" -q -y curl "${@}" || apt-get install -o "Dpkg::Use-Pty=0" -q -y curl "${@}"
121+
curl -sL -o /oras.tar.gz ${oras_down_url}
122+
tar -xvf /oras.tar.gz -C /usr/local/bin/ oras
123+
rm -rf /oras.tar.gz
124+
chmod +x /usr/local/bin/oras
125+
echo -n "ORAS version: " && oras version
126+
DOWNLOAD_HELPER_SCRIPT
127+
log debug "Created apt-oras helper script '${fn}'"
128+
}
129+
130+
# A huge hack to force the architecture of a Docker image to a specific value.
131+
# This is required for the LinuxKit kernel images: LK expects them to have the correct arch, despite the
132+
# actual contents always being the same. Docker's buildkit tags a locally built image with the host arch.
133+
# Thus change the host arch to the expected arch in the image's manifests via a dump/reimport.
134+
function ensure_docker_image_architecture() {
135+
declare kernel_oci_image="$1"
136+
declare expected_arch="$2"
137+
138+
# If the host arch is the same as the expected arch, no need to do anything
139+
if [[ "$(uname -m)" == "${expected_arch}" ]]; then
140+
log info "Host architecture is already ${expected_arch}, no need to rewrite Docker image ${kernel_oci_image}"
141+
return 0
142+
fi
143+
144+
log info "Rewriting Docker image ${kernel_oci_image} to architecture ${expected_arch}, wait..."
145+
146+
# Create a temporary directory, use mktemp
147+
declare -g tmpdir
148+
tmpdir="$(mktemp -d)"
149+
log debug "Created temporary directory: ${tmpdir}"
150+
151+
# Export the image to a tarball
152+
docker save -o "${tmpdir}/original.tar" "${kernel_oci_image}"
153+
154+
# Untag the hostarch image
155+
docker rmi "${kernel_oci_image}"
156+
157+
# Create a working dir under the tmpdir
158+
mkdir -p "${tmpdir}/working"
159+
160+
# Extract the tarball into the working dir
161+
tar -xf "${tmpdir}/original.tar" -C "${tmpdir}/working"
162+
log debug "Extracted tarball to ${tmpdir}/working"
163+
164+
# Remove the original tarball
165+
rm -f "${tmpdir}/original.tar"
166+
167+
declare working_blobs_dir="${tmpdir}/working/blobs/sha256"
168+
169+
# Find all files under working_blobs_dir which are smaller than 2048 bytes
170+
# Use mapfile to create an array of files
171+
declare -a small_files
172+
mapfile -t small_files < <(find "${working_blobs_dir}" -type f -size -2048c)
173+
log debug "Found small blob files: ${small_files[*]}"
174+
175+
# Replace the architecture in each of the small files
176+
for file in "${small_files[@]}"; do
177+
log debug "Replacing architecture in ${file}"
178+
sed -i "s|\"architecture\":\".....\"|\"architecture\":\"${expected_arch}\"|g" "${file}" # 🤮
179+
done
180+
181+
# Create a new tarball with the modified files
182+
tar -cf "${tmpdir}/modified.tar" -C "${tmpdir}/working" .
183+
log debug "Created modified tarball: ${tmpdir}/modified.tar"
184+
185+
# Remove the working directory
186+
rm -rf "${tmpdir}/working"
187+
188+
# Import the modified tarball back into the local cache
189+
docker load -i "${tmpdir}/modified.tar"
190+
191+
# Remove the temporary directory, completely
192+
rm -rf "${tmpdir}"
193+
194+
log info "Rewrote Docker image ${kernel_oci_image} to architecture ${expected_arch}."
195+
}

bash/hook-lk-containers.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ function build_hook_linuxkit_container() {
6666
return 0
6767
}
6868

69-
7069
function push_hook_linuxkit_container() {
7170
declare container_oci_ref="${1}"
7271

bash/kernel.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function kernel_build() {
3333
log debug "Kernel build method: ${kernel_info[BUILD_FUNC]}"
3434
"${kernel_info[BUILD_FUNC]}"
3535

36-
# Push it to the OCI registry
36+
# Push it to the OCI registry; this discards the os/arch information that BuildKit generates
3737
if [[ "${DO_PUSH:-"no"}" == "yes" ]]; then
3838
log info "Kernel built; pushing to ${kernel_oci_image}"
3939
docker push "${kernel_oci_image}"

0 commit comments

Comments
 (0)