Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ jobs:
# is safe here for two reasons: (a) the env: block is step-scoped, so
# mix release in the next step does not inherit TARGET_ABI, and (b) mix
# deps.compile only compiles the named deps, not exqlite, so
# cc_precompiler never runs during this step. mix release sees mdex_native already
# compiled and skips it, bundling the musl .so from _build/prod.
# cc_precompiler never runs during this step. mix release sees
# mdex_native already compiled and skips it, bundling the musl .so
# from _build/prod.
# rustler_precompiled is listed first because mix deps.compile only
# compiles the named deps (not their transitive deps), and mdex_native
# uses it via `use RustlerPrecompiled` at compile time. See EXT-7.
Expand All @@ -128,6 +129,14 @@ jobs:
MIX_ENV: prod
TARGET_ABI: musl
run: mix deps.compile rustler_precompiled mdex_native
-
# mdex_native's precompiled musl NIF dynamically needs libgcc_s.
# Burrito's musl loader otherwise finds the Ubuntu host's glibc copy
# first and rejects it at runtime. Bundle Alpine's compatible runtime
# under a unique name and patch the NIF to select it deterministically.
if: startsWith(matrix.target, 'linux_')
name: Bundle mdex_native's musl libgcc runtime
run: ../ci/bundle_mdex_musl_libgcc.sh
-
name: Build the ${{ matrix.target }} target
run: MIX_ENV=prod BURRITO_TARGET=${{ matrix.target }} mix release lc
Expand Down Expand Up @@ -157,19 +166,16 @@ jobs:
fi
fi
-
# Smoke-test the linux_aarch64 binary on the native ARM64 runner:
# proves the binary executes (right architecture), the OTP app boots,
# and all NIFs (exqlite via elixir_make/Zig, mdex_native via
# rustler_precompiled) load correctly. `lc version` runs in
# interactive mode (no LINEAR_CLI_DAEMON=true), starts an empty
# supervisor, prints the version, and exits 0 - no API key or network
# needed. Only runs for linux_aarch64 because that's the only target
# whose runner architecture matches the binary (macos/Windows smoke
# tests are out of scope for CRY-48).
if: matrix.target == 'linux_aarch64'
name: Smoke-test the linux_aarch64 binary boots and NIFs load
# Both Linux targets build on native matching runners. `lc version`
# boots the application and performs a hidden Marcli render, forcing
# mdex_native and its NIF to load before the command can exit 0. No API
# key or network is needed. macOS/Windows runtime smoke tests remain
# out of scope because their hosted runners are not all native to the
# configured Burrito targets.
if: startsWith(matrix.target, 'linux_')
name: Smoke-test the ${{ matrix.target }} binary and Markdown NIF
timeout-minutes: 1
run: ./burrito_out/lc_linux_aarch64 version
run: ./burrito_out/lc_${{ matrix.target }} version
-
# Handed off to burrito-package below, which needs every target's
# binary gathered back into one place before it can build the
Expand Down Expand Up @@ -351,6 +357,12 @@ jobs:
TARGET_ABI: musl
run: mix deps.compile rustler_precompiled mdex_native
working-directory: app
-
# Apply the same musl libgcc repair as the standalone Linux binaries;
# this release is copied into the Alpine container image below.
name: Bundle mdex_native's musl libgcc runtime
run: ../ci/bundle_mdex_musl_libgcc.sh
working-directory: app
-
name: Build the linux_x86_64 target (container's payload)
run: MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release lc
Expand All @@ -362,11 +374,10 @@ jobs:
run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}"
-
# Verify the musl NIF actually loads on Alpine before publishing.
# Analogous to the linux_aarch64 native smoke test in burrito-build:
# `lc version` boots the OTP app, loads all NIFs (exqlite via
# elixir_make/Zig, mdex_native via rustler_precompiled with the musl
# NIF bundled by the pre-compile step above), prints the version, and
# exits 0. No API key or network needed. ci/build_image.sh prefers
# `lc version` boots the OTP app and performs a hidden Markdown render,
# forcing mdex_native's NIF and its bundled libgcc runtime to load.
# It then prints the version and exits 0; no API key or network is
# needed. ci/build_image.sh prefers
# Podman, so the image lives in Podman's local storage; we run it
# directly rather than loading a tarball. LINEAR_CLI_DAEMON is
# overridden to false (the image bakes in true so the default CMD
Expand Down
10 changes: 9 additions & 1 deletion app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ defmodule LinearCli.CLI.Commands do
@doc """
Ported from commands/version.rb, extended to respect the global
`--output json` option like every other command does - previously
ignored it and always printed plain text.
ignored it and always printed plain text. The hidden Markdown render makes
this command a complete release smoke test for mdex_native's NIF as well as
the application boot path.
"""
def version(%{options: options}) do
verify_markdown_runtime!()
version = to_string(Application.spec(:linear_cli, :vsn))

if options.output == "json" do
Expand All @@ -32,6 +35,11 @@ defmodule LinearCli.CLI.Commands do
:ok
end

defp verify_markdown_runtime! do
_rendered = Marcli.render("runtime check", escape_sequences: false)
:ok
end

@doc "Ported from commands/team/list.rb. Ruby's `--mine` defaults true."
def team_list(%{flags: flags, options: options}) do
result = if flags.no_mine, do: Linear.teams(), else: Linear.my_teams()
Expand Down
75 changes: 75 additions & 0 deletions ci/bundle_mdex_musl_libgcc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash

set -euo pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
repo_root=$(cd -- "$script_dir/.." && pwd)
native_dir=${1:-"$repo_root/app/_build/prod/lib/mdex_native/priv/native"}

if [ ! -d "$native_dir" ]; then
printf 'mdex_native directory does not exist: %s\n' "$native_dir" >&2
exit 1
fi

native_dir=$(cd -- "$native_dir" && pwd)
shopt -s nullglob
musl_nifs=("$native_dir"/libmdex_native_nif-*-unknown-linux-musl.so)
shopt -u nullglob

if [ "${#musl_nifs[@]}" -ne 1 ]; then
printf 'expected exactly one mdex_native musl NIF in %s; found %d\n' \
"$native_dir" "${#musl_nifs[@]}" >&2
exit 1
fi

# Rust's dynamically linked musl cdylibs depend on libgcc_s. Burrito starts
# its Linux ERTS with a musl loader but puts the host library directories on
# LD_LIBRARY_PATH, where a glibc libgcc_s may be found first. Give Alpine's
# musl-compatible runtime a unique dependency name and keep it beside the NIF
# so the loader cannot accidentally select the host copy.
nif_name=$(basename -- "${musl_nifs[0]}")
libgcc_name=libmdex_musl_libgcc_s.so.1

if command -v podman >/dev/null 2>&1 && podman info >/dev/null 2>&1; then
container_runtime=podman
elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
container_runtime=docker
else
printf 'podman or docker is required to bundle the musl libgcc runtime\n' >&2
exit 1
fi

container_args=(run --rm)

if [ "$container_runtime" = podman ]; then
container_args+=(--security-opt label=disable)
fi

container_args+=(-v "$native_dir:/native")

# The single-quoted body is intentionally expanded by the container's shell.
# shellcheck disable=SC2016
"$container_runtime" "${container_args[@]}" alpine:3.22 sh -euxc '
apk add --no-cache libgcc patchelf

nif="/native/$1"
bundled_libgcc="/native/$2"
install -m 0755 /usr/lib/libgcc_s.so.1 "$bundled_libgcc"

# Set RUNPATH before growing DT_NEEDED. With patchelf 0.18, doing these
# two mutations in the opposite order can produce a loadable NIF that
# crashes on its first call.
patchelf --set-rpath "\$ORIGIN" "$nif"

if patchelf --print-needed "$nif" | grep -Fxq libgcc_s.so.1; then
patchelf --replace-needed libgcc_s.so.1 "$2" "$nif"
elif ! patchelf --print-needed "$nif" | grep -Fxq "$2"; then
printf "mdex_native NIF has no expected libgcc dependency: %s\\n" "$nif" >&2
exit 1
fi

patchelf --print-needed "$nif" | grep -Fxq "$2"
test "$(patchelf --print-rpath "$nif")" = "\$ORIGIN"
' sh "$nif_name" "$libgcc_name"

printf 'bundled musl libgcc runtime for %s\n' "${musl_nifs[0]}"