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
100 changes: 53 additions & 47 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,12 @@ jobs:
# a release with a target missing.
#
# Each target runs on its matching OS runner so rustler_precompiled
# (used by mdex_native) downloads the correct precompiled NIF for the
# target platform. Cross-compiling from Ubuntu caused the Linux NIF to
# be bundled into macOS/Windows releases, making MDEx unavailable at
# runtime (see CRY-40). Linux targets also get a pre-compile step that
# forces the musl NIF variant, since Ubuntu runners auto-select the
# glibc NIF but Burrito Linux binaries run on musl-based systems (see
# EXT-7).
# (used by mdex_native and makeup_syntect) downloads the correct
# precompiled NIF for the target platform. Cross-compiling from Ubuntu
# caused Linux NIFs to be bundled into macOS/Windows releases, making
# them unavailable at runtime (see CRY-40). Linux targets additionally
# prepare musl variants, since Ubuntu runners select glibc while Burrito
# Linux binaries run on musl-based systems (see EXT-7).
matrix:
include:
- target: macos_aarch64
Expand Down Expand Up @@ -109,35 +108,35 @@ jobs:
-
run: mix deps.get
-
# rustler_precompiled resolves the NIF target at compile time from
# :erlang.system_info(:system_architecture), which is always glibc on
# Ubuntu runners. Burrito bundles its own musl runtime for Linux and
# the container runs Alpine - both need the musl NIF, not glibc.
# TARGET_ABI=musl tells rustler_precompiled to download the musl
# variant. cc_precompiler (used by exqlite) also reads TARGET_ABI, but
# 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.
# 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.
if: startsWith(matrix.target, 'linux_')
name: Pre-compile mdex_native with the musl NIF
# makeup_syntect calls its NIF while compiling generated module docs,
# so it cannot compile against a musl artifact on the glibc runner.
# Compile it for the host first; prepare_musl_nifs.sh replaces only
# the packaged NIF after compilation. castore must be named before
# rustler_precompiled 0.8.x because deps.compile does not compile
# transitive dependencies automatically.
name: Compile makeup_syntect with its host NIF
env:
MIX_ENV: prod
run: mix deps.compile castore rustler_precompiled makeup_syntect
-
# rustler_precompiled resolves the NIF target at compile time from the
# Ubuntu host unless TARGET_ABI overrides it. This is safe as a
# step-scoped variable: cc_precompiler (used by exqlite) never sees it.
# mix release then reuses the completed dependency in _build/prod.
if: startsWith(matrix.target, 'linux_')
name: Pre-compile mdex_native with its musl NIF
env:
MIX_ENV: prod
TARGET_ABI: musl
run: mix deps.compile rustler_precompiled mdex_native
run: mix deps.compile 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.
# Replace makeup_syntect's host NIF with its checksummed musl artifact,
# then bundle and select Alpine's compatible libgcc runtime for both
# Rust NIFs. Burrito's musl loader otherwise finds Ubuntu's glibc copy.
if: startsWith(matrix.target, 'linux_')
name: Bundle mdex_native's musl libgcc runtime
run: ../ci/bundle_mdex_musl_libgcc.sh
name: Install and repair the musl NIFs
run: ../ci/prepare_musl_nifs.sh
-
name: Build the ${{ matrix.target }} target
run: MIX_ENV=prod BURRITO_TARGET=${{ matrix.target }} mix release lc
Expand Down Expand Up @@ -168,9 +167,10 @@ jobs:
fi
-
# Both Linux targets build on native matching runners. `lc version`
# boots the application and performs a hidden syntax-highlighted Marcli
# render, forcing mdex_native, its NIF, Makeup, and the Elixir lexer to
# load before the command can exit 0. No API key or network is needed.
# boots the application and performs hidden syntax-highlighted Marcli
# renders for both Elixir and Ruby, forcing mdex_native, makeup_syntect,
# both NIFs, Makeup, and both lexers 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_')
Expand Down Expand Up @@ -349,21 +349,26 @@ jobs:
run: mix deps.get
working-directory: app
-
# Same reasoning as the burrito-build pre-compile step: Ubuntu runners
# default to the glibc NIF, but the container image runs Alpine (musl).
# rustler_precompiled listed first for the same reason as there. See
# EXT-7.
name: Pre-compile mdex_native with the musl NIF
# Same two-stage preparation as the Linux burrito-build legs: Syntect
# must finish compiling with its host NIF before that artifact can be
# replaced with the musl build.
name: Compile makeup_syntect with its host NIF
env:
MIX_ENV: prod
run: mix deps.compile castore rustler_precompiled makeup_syntect
working-directory: app
-
name: Pre-compile mdex_native with its musl NIF
env:
MIX_ENV: prod
TARGET_ABI: musl
run: mix deps.compile rustler_precompiled mdex_native
run: mix deps.compile 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
# Install Syntect's musl artifact and apply the same libgcc repair to
# both Rust NIFs as the standalone Linux binaries.
name: Install and repair the musl NIFs
run: ../ci/prepare_musl_nifs.sh
working-directory: app
-
name: Build the linux_x86_64 target (container's payload)
Expand All @@ -376,10 +381,11 @@ jobs:
run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}"
-
# Verify the musl NIF actually loads on Alpine before publishing.
# `lc version` boots the OTP app and performs a hidden syntax-highlighted
# Markdown render, forcing mdex_native's NIF, its bundled libgcc runtime,
# Makeup, and the Elixir lexer to load. It then prints the version and
# exits 0; no API key or network is needed. ci/build_image.sh prefers
# `lc version` boots the OTP app and performs hidden syntax-highlighted
# Markdown renders, forcing the MDEx and Syntect NIFs, their bundled
# libgcc runtimes, Makeup, and the Elixir/Ruby lexers 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
15 changes: 9 additions & 6 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ 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. The hidden Markdown render makes
this command a complete release smoke test for mdex_native's NIF, Marcli's
syntax-highlighting lexer, and the application boot path.
ignored it and always printed plain text. The hidden Markdown renders make
this command a complete release smoke test for the MDEx and Syntect NIFs,
Marcli's syntax-highlighting integration, and the application boot path.
"""
def version(%{options: options}) do
verify_markdown_runtime!()
Expand All @@ -39,10 +39,13 @@ defmodule LinearCli.CLI.Commands do

defp verify_markdown_runtime! do
theme = Marcli.Theme.default()
rendered = Marcli.render("```elixir\ndef smoke, do: :ok\n```")
highlighted_keyword = theme.syntax.keyword_declaration <> "def" <> theme.reset
elixir = Marcli.render("```elixir\ndef smoke, do: :ok\n```")
ruby = Marcli.render("```ruby\ndef smoke; :ok; end\n```")

unless String.contains?(rendered, highlighted_keyword) do
elixir_keyword = theme.syntax.keyword_declaration <> "def" <> theme.reset
ruby_keyword = theme.syntax.keyword_type <> "def" <> theme.reset

unless String.contains?(elixir, elixir_keyword) and String.contains?(ruby, ruby_keyword) do
raise "Markdown syntax-highlighting runtime is unavailable"
end

Expand Down
1 change: 1 addition & 0 deletions app/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule LinearCli.MixProject do
[
{:marcli, "~> 0.3"},
{:makeup_elixir, "~> 1.0"},
{:makeup_syntect, "~> 0.1"},
{:owl, "~> 0.13"},
{:optimus, "~> 0.6"},
{:req, "~> 0.7"},
Expand Down
4 changes: 3 additions & 1 deletion app/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"ash": {:hex, :ash, "3.31.3", "b36672bff745eadd52265d8b0a303f98ba26a31173f4116ebd2df7be0cbafeff", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c105da2080475d114908c32e70970e69c857a9068a577a4e99dbf2801baa11e"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"burrito": {:hex, :burrito, "1.6.0", "7af0a75f11680e8a6e9c01370c9af51cb9d0e15b3226eddf4f438dbc68570520", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "e636a00b032c45a69ff755d9fc53fa5fdc9e1d21bdbd229075fe4a15b05355fe"},
"castore": {:hex, :castore, "1.0.21", "0a0e8330dc267a40a3b7ad86d39302764bb71758172904e6a59d5ad6443ce307", [:mix], [], "hexpm", "e42e22723e25dbd46876d056a03f685513d6e98f6b5e555dc551321decd76c5c"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"},
"crux": {:hex, :crux, "0.1.4", "1fa21f5ca886d3498f83871a3ef19379b80abf8cfcf2ed32007e80279e714f1e", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "ff7d880cf732d82360aa81e439b8d4831cd30768061c9233f90c97e10162b9c0"},
Expand All @@ -24,6 +25,7 @@
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_syntect": {:hex, :makeup_syntect, "0.1.4", "e1230c9e0513c667b226b21c83eb182e1ab581f65af9441edab1f9ac626acba6", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}, {:rustler, "~> 0.37.1", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8.2", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5b624a434d9665786b9a352a5f3502b6c98e1996ede9936b20035ec140daef70"},
"marcli": {:hex, :marcli, "0.3.1", "183d2c674e416cdd90a5b395d60d573ad5d4381ec18239de2b2cfa249d926d7c", [:mix], [{:color, "~> 0.3", [hex: :color, repo: "hexpm", optional: true]}, {:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: true]}, {:mdex, "~> 0.11", [hex: :mdex, repo: "hexpm", optional: false]}], "hexpm", "731383f9b9bdfd13affe2d43a3cef441440c4a9182cb59724478e51b85e2c7d4"},
"mdex": {:hex, :mdex, "0.13.5", "c1c94d230ccaab01ad0c68090d3b31613c10ece1844f32b55895da4ce0c63029", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.0 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "c57409fb6b34fbc58fbce0a6da670c9a4b5a2e94f86abdc56e9e213ed74620f2"},
"mdex_native": {:hex, :mdex_native, "0.2.7", "46203ee9c4fe2c94feef26e4d976235ad022b4770d3d307fa6b445d293b2571f", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "05efa68775644ef6be6109f76c1f5f8778ff7e7ed9fe4a03541758158172bc77"},
Expand All @@ -45,7 +47,7 @@
"reactor": {:hex, :reactor, "1.0.6", "546a87255693bcee99451d022cc86927161cdb527f5216d4dcac2b31e08eb122", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "e3f8fd8e870c2b011316ca2ac422bb4bc710cac9f02c578f8526f7a6348d932b"},
"req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"},
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"},
"sbom": {:hex, :sbom, "0.10.0", "b99be5407bc196d0ad71b8061126a67aae46dc3bfaa852b4c1c04645dd1ad984", [:mix], [{:hex_core, "~> 0.15.0", [hex: :hex_core, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:optimus, "~> 0.6.1", [hex: :optimus, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.16.0", [hex: :protobuf, repo: "hexpm", optional: false]}, {:purl, "~> 0.3.0", [hex: :purl, repo: "hexpm", optional: false]}], "hexpm", "a8116ef965c1ebd103e223545794bd0a6691edd3ec678ec07972d473e2badc95"},
"sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"},
"spark": {:hex, :spark, "2.7.2", "36becc6ff03b40908cc821d403d7f06d893498e293d2f718afc6ca097fcb9d93", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "adb323ddbf9dbbe326f9e5def54ac96c47911e852b2c270bb19a5147c56f1b45"},
Expand Down
Loading
Loading