diff --git a/README.md b/README.md index 78677bb..96624f4 100644 --- a/README.md +++ b/README.md @@ -55,26 +55,29 @@ Pix requires a `docker` engine installed and running on your host system. $ mix escript.install github athonet-open/pix ref vX.Y.Z ``` -#### Option 2: Docker Installation +Requires Erlang/Elixir to be installed on the host. -```bash -$ docker run --rm -it \ - --volume $PWD:/$PWD --workdir /$PWD \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - --volume $SSH_AUTH_SOCK:$SSH_AUTH_SOCK \ - --env SSH_AUTH_SOCK=$SSH_AUTH_SOCK \ - ghcr.io/athonet-open/pix:X.Y.Z "$@" -``` +#### Option 2: Wrapper Script + +A self-managing shell script that automatically resolves the latest version, builds the Docker image from source, and keeps itself up to date. -Important considerations: -- Docker engine access is required via either Docker Socket Mounting (DooD) or Docker-in-Docker (dind) -- For SSH access, forward the SSH agent socket to the Pix container -- For macOS users with Docker Desktop: ```bash ---volume /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock \ ---env SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock +$ curl -fsSL https://raw.githubusercontent.com/athonet-open/pix/main/bin/pix -o /usr/local/bin/pix && chmod +x /usr/local/bin/pix ``` +Requirements: `bash`, `docker`, `git`, `curl`. + +On first run, the script will: +1. Detect the latest release tag from GitHub +2. Build the Pix Docker image locally from source +3. Run the requested command + +On subsequent runs, the cached image is reused. When a new version is released, the script +automatically updates itself and rebuilds the image. + +The script handles Docker socket mounting, SSH agent forwarding (with macOS Docker Desktop support), +and mounts `~/.ssh`, `~/.gitconfig*`, and `~/.config/pix/settings.exs` if present. + ### Shell completion Shell completion scripts are available for the following shells: diff --git a/bin/pix b/bin/pix new file mode 100755 index 0000000..8d0febb --- /dev/null +++ b/bin/pix @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# pix - wrapper script for running Pix via Docker +# +# Automatically resolves the latest version from GitHub tags, +# self-updates the script and builds the image from source when a new version is detected. +# +# Install: curl -fsSL https://raw.githubusercontent.com/athonet-open/pix/main/bin/pix -o /usr/local/bin/pix && chmod +x /usr/local/bin/pix +set -euo pipefail + +readonly PIX_REPO="https://github.com/athonet-open/pix.git" + +preflight_checks() { + local -a required=(docker git) + for cmd in "${required[@]}"; do + command -v "$cmd" >/dev/null 2>&1 || { echo "error: $cmd is not installed or not in PATH" >&2; exit 1; } + done + docker info >/dev/null 2>&1 || { echo "error: docker daemon is not running" >&2; exit 1; } +} + +resolve_version() { + local latest_tag + latest_tag=$(git ls-remote --tags --sort=-v:refname "$PIX_REPO" 'v*' 2>/dev/null \ + | head -1 | sed 's|.*refs/tags/||') + [[ -z "$latest_tag" ]] && { echo "error: could not determine latest pix version from $PIX_REPO" >&2; exit 1; } + + local -n _tag=$1 _ver=$2 _img=$3 + _tag="$latest_tag" + _ver="${latest_tag#v}" + _img="athonet-open/pix:${_ver}" +} + +ensure_image() { + local tag="$1" version="$2" image="$3" + shift 3 + + docker image inspect "$image" >/dev/null 2>&1 && return + + echo "pix: new version detected ($version), updating..." >&2 + + local script_url="https://raw.githubusercontent.com/athonet-open/pix/${tag}/bin/pix" + curl -fsSL "$script_url" -o "$0" + chmod +x "$0" + echo "pix: script updated to $version" >&2 + + echo "pix: building $image from source..." >&2 + local build_dir + build_dir=$(mktemp -d) + trap "rm -rf '$build_dir'" EXIT + git clone --depth 1 --branch "$tag" "$PIX_REPO" "$build_dir" + docker build --build-arg "VERSION=${version}" -t "$image" "$build_dir" + echo "pix: image $image built successfully" >&2 + + exec "$0" "$@" +} + +docker_run_opts() { + local -n _opts=$1 + local docker_sock="/var/run/docker.sock" + + [[ -S "$docker_sock" ]] || { echo "error: docker socket not found at $docker_sock" >&2; exit 1; } + _opts=(--volume "$docker_sock:$docker_sock") + + # SSH agent forwarding + case "$(uname -s)" in + Darwin) + _opts+=(--volume /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock) + _opts+=(--env SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock) + ;; + *) + if [[ -n "${SSH_AUTH_SOCK:-}" ]]; then + if [[ ! -S "$SSH_AUTH_SOCK" ]]; then + echo "warning: SSH_AUTH_SOCK is set but $SSH_AUTH_SOCK is not a valid socket, skipping SSH forwarding" >&2 + else + _opts+=(--volume "$SSH_AUTH_SOCK:$SSH_AUTH_SOCK" --env "SSH_AUTH_SOCK=$SSH_AUTH_SOCK") + fi + fi + ;; + esac + + # User settings + local pix_settings="$HOME/.config/pix/settings.exs" + [[ -f "$pix_settings" ]] && _opts+=(--volume "$pix_settings:$pix_settings:ro") + + # SSH keys + [[ -d "$HOME/.ssh" ]] && _opts+=(--volume "$HOME/.ssh:$HOME/.ssh:ro") + + # Git config + local f + for f in "$HOME"/.gitconfig*; do + [[ -f "$f" ]] && _opts+=(--volume "$f:$f:ro") + done + + # TTY + [[ -t 0 ]] && _opts+=(-it) + + # Extra user options + if [[ -n "${PIX_DOCKER_RUN_OPTS:-}" ]]; then + local -a extra + read -ra extra <<< "$PIX_DOCKER_RUN_OPTS" + _opts+=("${extra[@]}") + fi +} + +main() { + preflight_checks + + local tag version image + resolve_version tag version image + + ensure_image "$tag" "$version" "$image" "$@" + + local -a opts + docker_run_opts opts + + exec docker run --rm \ + --env HOME="$HOME" \ + --env PIX_HOST_OS="$(uname -s)" \ + --volume "$PWD:/$PWD" --workdir "/$PWD" \ + "${opts[@]}" \ + "$image" "$@" +} + +main "$@" \ No newline at end of file diff --git a/lib/pix/command/help.ex b/lib/pix/command/help.ex index ecbf95e..cc4e6e7 100644 --- a/lib/pix/command/help.ex +++ b/lib/pix/command/help.ex @@ -38,6 +38,8 @@ defmodule Pix.Command.Help do #{_var("PIX_DOCKER_BUILDX_DEBUG")}: Set to "true" to enable `docker buildx debug`. If enabled and an error occurs in a `RUN` command, an interactive shell is presented which can be used for investigating the error interactively. + #{_var("PIX_HOST_OS")}: Override the detected host OS (e.g. "Darwin", "Linux"). + Useful when running pix inside a container via the wrapper script. """) :ok diff --git a/lib/pix/env.ex b/lib/pix/env.ex index a1c99fb..8cc64ea 100644 --- a/lib/pix/env.ex +++ b/lib/pix/env.ex @@ -87,12 +87,22 @@ defmodule Pix.Env do end @doc """ - Returns the operating system name by executing `uname -s`. + Returns the operating system name. + Can be overridden by setting `PIX_HOST_OS` environment variable, + which is useful when pix runs inside a container (e.g. via the wrapper script) + but needs to report the actual host OS. + Falls back to `uname -s`. """ @spec os :: String.t() def os do - {res, 0} = System.cmd("uname", ~w[-s]) - String.trim(res) + case System.get_env("PIX_HOST_OS") do + nil -> + {res, 0} = System.cmd("uname", ~w[-s]) + String.trim(res) + + os -> + os + end end @doc """ diff --git a/shell_completions/pix.fish b/shell_completions/pix.fish index 18fed92..bb2745d 100644 --- a/shell_completions/pix.fish +++ b/shell_completions/pix.fish @@ -3,6 +3,18 @@ complete -c pix -e complete -c pix -f -d "Pipelines for buildx" +# Function to find the pipeline name from the command line +# Scans tokens starting after "pix ", returning the first non-flag positional argument +function __pix_find_pipeline + set --local cmd_tokens (commandline --tokens-expanded) + for i in (seq 3 (count $cmd_tokens)) + if not string match -q -- "-*" $cmd_tokens[$i] + echo $cmd_tokens[$i] + return + end + end +end + # Function to get available pipelines function __pix_get_pipelines command pix __complete_fish pipeline 2>/dev/null @@ -10,21 +22,18 @@ end # Function to get available targets for a pipeline function __pix_get_run_targets - set --local cmd_tokens (commandline --tokens-expanded) - - # Pipeline should be the last argument - set pipeline $cmd_tokens[-1] + set --local pipeline (__pix_find_pipeline) - command pix __complete_fish target $pipeline 2>/dev/null + if test -n "$pipeline" + command pix __complete_fish target $pipeline 2>/dev/null + end end # Function to get available args for a target of a pipeline function __pix_get_run_target_args + set --local pipeline (__pix_find_pipeline) set --local cmd_tokens (commandline --tokens-expanded) - # Pipeline should be the last argument - set pipeline $cmd_tokens[-1] - # Find index of "--target" set --local target_index 0 set --local target "" @@ -65,27 +74,27 @@ complete -c pix -n "__fish_seen_subcommand_from ls" -l "hidden" -d "Show also pr # graph command options complete -c pix -n "__fish_seen_subcommand_from graph" -a "(__pix_get_pipelines)" -d "Pipeline" -complete -c pix -n "__fish_seen_subcommand_from graph" -l "format" -d "Output format" -a "pretty dot" +complete -c pix -n "__fish_seen_subcommand_from graph" -l "format" -rf -d "Output format" -a "pretty dot" # run command options complete -c pix -n "__fish_seen_subcommand_from run" -a "(__pix_get_pipelines)" -d "Pipeline" complete -c pix -n "__fish_seen_subcommand_from run" -l "output" -d "Output the target artifacts under .pipeline/output directory" complete -c pix -n "__fish_seen_subcommand_from run" -l "ssh" -d "Forward SSH agent/keys to buildx build (default, or id=path)" -rf -complete -c pix -n "__fish_seen_subcommand_from run" -l "arg" -d "Set one or more pipeline ARG (format KEY=value)" -a "(__pix_get_run_target_args)" -complete -c pix -n "__fish_seen_subcommand_from run" -l "progress" -d "Set type of progress output" -a "auto plain tty rawjson" -complete -c pix -n "__fish_seen_subcommand_from run" -l "secret" -d "Forward one or more secrets to `buildx build`" -complete -c pix -n "__fish_seen_subcommand_from run" -l "target" -d "Run PIPELINE for a specific TARGET" -a "(__pix_get_run_targets)" -complete -c pix -n "__fish_seen_subcommand_from run" -l "tag" -d "Tag the TARGET's docker image (requires --target)" +complete -c pix -n "__fish_seen_subcommand_from run" -l "arg" -rf -d "Set one or more pipeline ARG (format KEY=value)" -a "(__pix_get_run_target_args)" +complete -c pix -n "__fish_seen_subcommand_from run" -l "progress" -rf -d "Set type of progress output" -a "auto plain tty rawjson" +complete -c pix -n "__fish_seen_subcommand_from run" -l "secret" -rf -d "Forward one or more secrets to `buildx build`" +complete -c pix -n "__fish_seen_subcommand_from run" -l "target" -rf -d "Run PIPELINE for a specific TARGET" -a "(__pix_get_run_targets)" +complete -c pix -n "__fish_seen_subcommand_from run" -l "tag" -rf -d "Tag the TARGET's docker image (requires --target)" complete -c pix -n "__fish_seen_subcommand_from run" -l "save" -d "Save the TARGET's docker image to a file (requires --target and --tag)" -rF complete -c pix -n "__fish_seen_subcommand_from run" -l "no-cache" -d "Do not use cache when building the image" -complete -c pix -n "__fish_seen_subcommand_from run" -l "no-cache-filter" -d "Do not cache specified targets" -a "(__pix_get_run_targets)" +complete -c pix -n "__fish_seen_subcommand_from run" -l "no-cache-filter" -rf -d "Do not cache specified targets" -a "(__pix_get_run_targets)" # shell command options complete -c pix -n "__fish_seen_subcommand_from shell" -a "(__pix_get_pipelines)" -d "Pipeline" complete -c pix -n "__fish_seen_subcommand_from shell" -l "ssh" -d "Forward SSH agent/keys to shell container (default, or id=path)" -rf -complete -c pix -n "__fish_seen_subcommand_from shell" -l "arg" -d "Set one or more pipeline ARG (format KEY=value)" -complete -c pix -n "__fish_seen_subcommand_from shell" -l "secret" -d "Forward one or more secrets to `buildx build`" -complete -c pix -n "__fish_seen_subcommand_from shell" -l "target" -d "The shell target" +complete -c pix -n "__fish_seen_subcommand_from shell" -l "arg" -rf -d "Set one or more pipeline ARG (format KEY=value)" -a "(__pix_get_run_target_args)" +complete -c pix -n "__fish_seen_subcommand_from shell" -l "secret" -rf -d "Forward one or more secrets to `buildx build`" +complete -c pix -n "__fish_seen_subcommand_from shell" -l "target" -rf -d "The shell target" -a "(__pix_get_run_targets)" complete -c pix -n "__fish_seen_subcommand_from shell" -l "host" -d "Bind mount the current working dir" # upgrade command options @@ -94,5 +103,8 @@ complete -c pix -n "__fish_seen_subcommand_from upgrade" -l "dry-run" -d "Only c # cache command options complete -c pix -n "__fish_seen_subcommand_from cache" -a "info\t'Show info about the cache' update\t'Update the cache of remote git pipelines' clear\t'Clear the cache of remote git pipelines'" +# help command options +complete -c pix -n "__fish_seen_subcommand_from help" -a "ls graph run shell upgrade cache completion_script" -d "Command" + # completion_script command options complete -c pix -n "__fish_seen_subcommand_from completion_script" -a "fish" -d "Fish completion script"