diff --git a/.gitmodules b/.gitmodules index bc4d906..96fb69f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "vendor/owl"] path = vendor/owl url = git@github.com:fuelen/owl +[submodule "vendor/rubyists-homebrew-tap"] + path = vendor/rubyists-homebrew-tap + url = git@github.com:rubyists/homebrew-tap diff --git a/Readme.adoc b/Readme.adoc index 8d94d67..c5464ec 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -72,8 +72,6 @@ built with. The container image has its own separate SBOM (its OS packages - irrelevant if you're not using the container), published as a workflow artifact on the release's build rather than a release asset. -NOTE: A Homebrew tap is planned but not yet available. - On macOS, Gatekeeper blocks `lc` itself (the wrapper scripts are plain shell, so they're unaffected) since it isn't signed/notarized yet: @@ -82,18 +80,37 @@ so they're unaffected) since it isn't signed/notarized yet: $ xattr -d com.apple.quarantine /usr/local/bin/lc ---- -=== install.sh (builds from source, for machines without Homebrew) +=== Homebrew (macOS Apple Silicon, Linux x86_64) [source,sh] ---- -$ git clone https://github.com/rubyists/linear-cli-ex.git -$ cd linear-cli-ex -$ ./install.sh +$ brew tap rubyists/tap +$ brew install lc +---- + +See https://github.com/rubyists/homebrew-tap for the formula itself. + +=== install.sh (fetches a release binary, for machines without Homebrew) + +[source,sh] +---- +$ curl -fsSL https://raw.githubusercontent.com/rubyists/linear-cli-ex/main/install.sh | bash +---- + +Detects your platform, downloads and checksum-verifies the matching release +tarball (the same one the "Download a release binary" section above uses), +and installs `lc` plus the `bin/` wrapper scripts onto a directory already on +your `$PATH` - no Erlang/Elixir/Zig toolchain required. `LC_VERSION` pins a +specific release tag instead of the latest one; `LC_INSTALL_DIR` pins a +specific install directory. + +[source,sh] +---- +$ curl -fsSL https://raw.githubusercontent.com/rubyists/linear-cli-ex/main/uninstall.sh | bash ---- -Builds a native release via https://mise.jdx.dev[mise] and installs `lc` plus -the `bin/` wrapper scripts onto a directory already on your `$PATH`. Run -`./uninstall.sh` to remove exactly what it installed. +Removes exactly what `install.sh` installed, by replaying the manifest it +wrote - not a guess at where things ended up. === From Source (You are obviously a developer) diff --git a/install.sh b/install.sh index 4baad09..2ad245b 100755 --- a/install.sh +++ b/install.sh @@ -1,15 +1,20 @@ #!/usr/bin/env bash -# Builds a native Burrito release of `lc` for the current machine and -# installs it, plus the bin/ wrapper scripts (lcreate, lcls, lclose, -# lcomment, lproj), onto a directory already on $PATH. Fallback path for -# machines without Homebrew - see rubyists/homebrew-tap once it exists. +# Downloads a precompiled Burrito release of `lc` for the current machine +# and installs it, plus the bin/ wrapper scripts (lcreate, lcls, lclose, +# lcomment, lproj) already bundled in the same release tarball, onto a +# directory already on $PATH. Fallback path for machines without +# Homebrew - see rubyists/homebrew-tap for that. +# +# LC_VERSION pins a specific release tag (e.g. "v1.0.0") instead of the +# latest one. LC_INSTALL_DIR pins a specific install directory instead of +# the first writable $PATH entry found. # # Records exactly what it installed and where in a manifest file, so # uninstall.sh can remove precisely those files even if $PATH changes # between install and uninstall. -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -app_dir="$repo_root/app" +repo=rubyists/linear-cli-ex +version=${LC_VERSION:-latest} state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/linear-cli-ex" manifest="$state_dir/manifest" @@ -64,44 +69,85 @@ pick_install_dir() { printf '%s\n' "$HOME/.local/bin" } -build_lc() { - local target="$1" - - if ! cd "$app_dir" +# GNU coreutils' sha256sum vs. macOS's shasum -a 256 - both read the same +# " " format via `-c -` on stdin. +checksum_cmd() { + if command -v sha256sum >/dev/null 2>&1 then - printf 'error: could not cd into %s\n' "$app_dir" >&2 + printf 'sha256sum\n' + elif command -v shasum >/dev/null 2>&1 + then + printf 'shasum -a 256\n' + else + printf 'error: need sha256sum or shasum on $PATH to verify the download\n' >&2 exit 1 fi +} - rm -rf _build/prod - - have_mise=0 - if command -v mise >/dev/null 2>&1 +release_url() { + local asset="$1" + if [ "$version" = "latest" ] then - have_mise=1 + printf 'https://github.com/%s/releases/latest/download/%s\n' "$repo" "$asset" else - printf 'warning: mise not found on PATH; building with whatever Erlang/Elixir are active\n' >&2 + printf 'https://github.com/%s/releases/download/%s/%s\n' "$repo" "$version" "$asset" + fi +} + +# Downloads lc_.tar.gz + SHA256SUMS, verifies the tarball's +# checksum against just its own line (SHA256SUMS covers every target, +# not only the one being installed here), and extracts it. Prints the +# directory it extracted into. +fetch_lc() { + local target="$1" tarball tmp_dir sum_tool + tarball="lc_${target}.tar.gz" + + tmp_dir=$(mktemp -d) || { + printf 'error: could not create a temp directory\n' >&2 + exit 1 + } + trap 'rm -rf "$tmp_dir"' EXIT + + printf 'Downloading %s (%s)...\n' "$tarball" "$version" >&2 + + if ! curl -fsSL "$(release_url "$tarball")" -o "$tmp_dir/$tarball" + then + printf 'error: failed to download %s\n' "$tarball" >&2 + exit 1 fi - if [ "$have_mise" -eq 1 ] + if ! curl -fsSL "$(release_url "SHA256SUMS")" -o "$tmp_dir/SHA256SUMS" then - MIX_ENV=prod BURRITO_TARGET="$target" mise exec -- mix release lc --overwrite - else - MIX_ENV=prod BURRITO_TARGET="$target" mix release lc --overwrite + printf 'error: failed to download SHA256SUMS\n' >&2 + exit 1 + fi + + sum_tool=$(checksum_cmd) + + if ! grep -- " $tarball\$" "$tmp_dir/SHA256SUMS" | (cd "$tmp_dir" && $sum_tool -c -) >/dev/null + then + printf 'error: checksum verification failed for %s\n' "$tarball" >&2 + exit 1 fi - if [ "$?" -ne 0 ] + if ! tar -xzf "$tmp_dir/$tarball" -C "$tmp_dir" then - printf 'error: build failed (mix release lc, target %s)\n' "$target" >&2 + printf 'error: failed to extract %s\n' "$tarball" >&2 exit 1 fi + + printf '%s\n' "$tmp_dir" } +if ! command -v curl >/dev/null 2>&1 +then + printf 'error: curl is required\n' >&2 + exit 1 +fi + target=$(detect_target) || exit 1 install_dir=$(pick_install_dir) - -printf 'Building lc (%s)...\n' "$target" -build_lc "$target" +extracted_dir=$(fetch_lc "$target") || exit 1 if ! mkdir -p "$install_dir" then @@ -118,10 +164,7 @@ fi : > "$manifest" for name in lc lcreate lcls lclose lcomment lproj do - src="$app_dir/burrito_out/lc_${target}" - [ "$name" = lc ] || src="$repo_root/bin/$name" - - if ! install -m 755 "$src" "$install_dir/$name" + if ! install -m 755 "$extracted_dir/$name" "$install_dir/$name" then printf 'error: failed to install %s to %s\n' "$name" "$install_dir" >&2 exit 1 @@ -130,6 +173,15 @@ do printf '%s\n' "$install_dir/$name" >> "$manifest" done +# Gatekeeper blocks lc itself (a real Mach-O binary) on macOS since it +# isn't signed/notarized - the wrapper scripts are plain shell, so they're +# unaffected. Best-effort: xattr not existing/failing shouldn't fail the +# install, since the user can still do this by hand (see the README). +if [ "$(uname -s)" = Darwin ] +then + xattr -d com.apple.quarantine "$install_dir/lc" 2>/dev/null || true +fi + printf 'Installed lc, lcreate, lcls, lclose, lcomment, lproj to %s\n' "$install_dir" printf '(uninstall.sh will remove exactly these files - manifest at %s)\n' "$manifest" diff --git a/vendor/rubyists-homebrew-tap b/vendor/rubyists-homebrew-tap new file mode 160000 index 0000000..098d41c --- /dev/null +++ b/vendor/rubyists-homebrew-tap @@ -0,0 +1 @@ +Subproject commit 098d41c0e911e839aa3d16b99a9eceba22351892