tact 0.1.1

Terminal interface for Nanocodex
# syntax=docker/dockerfile:1.7

# Docker resolves FROM before it can read the tool manifest from the build context.
FROM debian:bookworm-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818 AS toolchain

ARG TARGETARCH

ENV DEBIAN_FRONTEND=noninteractive \
    RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/opt/cargo \
    UV_PYTHON_INSTALL_DIR=/opt/python \
    BINSTALL_DISABLE_TELEMETRY=true \
    PATH=/opt/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

COPY docker/dev/tools.toml /usr/local/share/tact-dev/tools.toml

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update \
    && apt-get install --yes --no-install-recommends ca-certificates curl gnupg \
    && llvm_key_url=$(awk -F' = ' '/^llvm_signing_key =/ { gsub(/"/, "", $2); print $2 }' \
        /usr/local/share/tact-dev/tools.toml) \
    && llvm_key_sha256=$(awk -F' = ' '/^llvm_signing_key_sha256 =/ { gsub(/"/, "", $2); print $2 }' \
        /usr/local/share/tact-dev/tools.toml) \
    && llvm_repository=$(awk -F' = ' '/^llvm_repository =/ { gsub(/"/, "", $2); print $2 }' \
        /usr/local/share/tact-dev/tools.toml) \
    && llvm_suite=$(awk -F' = ' '/^llvm_suite =/ { gsub(/"/, "", $2); print $2 }' \
        /usr/local/share/tact-dev/tools.toml) \
    && curl --fail --location --show-error "$llvm_key_url" --output /tmp/llvm.asc \
    && echo "$llvm_key_sha256  /tmp/llvm.asc" | sha256sum --check --strict \
    && gpg --batch --dearmor --output /usr/share/keyrings/llvm-archive-keyring.gpg \
        /tmp/llvm.asc \
    && rm /tmp/llvm.asc \
    && echo "deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg] $llvm_repository $llvm_suite main" \
        >/etc/apt/sources.list.d/llvm.list \
    && printf 'Package: clang-21 llvm-21 llvm-21-tools lld-21 lldb-21\nPin: origin apt.llvm.org\nPin-Priority: 1001\n' \
        >/etc/apt/preferences.d/llvm \
    && apt-get update \
    && apt-get install --yes --no-install-recommends \
        binutils \
        build-essential \
        ca-certificates \
        clang-21 \
        curl \
        elfutils \
        file \
        gdb \
        git \
        libdw1 \
        libelf1 \
        libnss-wrapper \
        libssl3 \
        libunwind8 \
        lld-21 \
        lldb-21 \
        llvm-21 \
        llvm-21-tools \
        pkg-config \
        procps \
        strace \
        unzip \
        xz-utils \
    && update-alternatives --install /usr/bin/clang clang /usr/bin/clang-21 210 \
    && update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-21 210 \
    && update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-21 210 \
    && update-alternatives --install /usr/bin/ld.lld ld.lld /usr/bin/ld.lld-21 210 \
    && update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 210 \
    && update-alternatives --install /usr/bin/lldb lldb /usr/bin/lldb-21 210 \
    && nss_wrapper=$(find /usr/lib -name libnss_wrapper.so -print -quit) \
    && test -n "$nss_wrapper" \
    && ln --symbolic "$nss_wrapper" /usr/local/lib/libnss_wrapper.so

RUN <<'EOF'
set -eu

manifest=/usr/local/share/tact-dev/tools.toml
manifest_value() {
    section="$1"
    key="$2"
    awk -v wanted_section="$section" -v wanted_key="$key" '
        $0 == "[" wanted_section "]" { in_section = 1; next }
        /^\[/ { in_section = 0 }
        in_section && $1 == wanted_key {
            sub(/^[^=]+=[[:space:]]*/, "")
            gsub(/^"|"$/, "")
            print
            exit
        }
    ' "$manifest"
}

case "$TARGETARCH" in
    amd64|arm64) architecture="$TARGETARCH" ;;
    *) echo "unsupported architecture: $TARGETARCH" >&2; exit 1 ;;
esac

uv_version=$(manifest_value uv version)
uv_target=$(manifest_value uv "${architecture}_target")
uv_sha256=$(manifest_value uv "${architecture}_sha256")
uv_archive="uv-${uv_target}.tar.gz"
curl --fail --location --show-error \
    "https://github.com/astral-sh/uv/releases/download/${uv_version}/${uv_archive}" \
    --output "/tmp/${uv_archive}"
echo "${uv_sha256}  /tmp/${uv_archive}" | sha256sum --check --strict
tar --extract --gzip --file "/tmp/${uv_archive}" --strip-components=1 \
    --directory /usr/local/bin
rm "/tmp/${uv_archive}"

python_version=$(manifest_value python version)
uv python install "$python_version"
python=$(find /opt/python -path '*/bin/python3.13' -print -quit)
test -n "$python"
ln --symbolic "$python" /usr/local/bin/python3
ln --symbolic "$python" /usr/local/bin/python

node_version=$(manifest_value node version)
node_target=$(manifest_value node "${architecture}_target")
node_sha256=$(manifest_value node "${architecture}_sha256")
node_archive="node-v${node_version}-linux-${node_target}.tar.xz"
curl --fail --location --show-error \
    "https://nodejs.org/dist/v${node_version}/${node_archive}" \
    --output "/tmp/${node_archive}"
echo "${node_sha256}  /tmp/${node_archive}" | sha256sum --check --strict
tar --extract --xz --file "/tmp/${node_archive}" --strip-components=1 \
    --directory /usr/local
rm "/tmp/${node_archive}"

bun_version=$(manifest_value bun version)
bun_target=$(manifest_value bun "${architecture}_target")
bun_sha256=$(manifest_value bun "${architecture}_sha256")
bun_archive="bun-linux-${bun_target}.zip"
curl --fail --location --show-error \
    "https://github.com/oven-sh/bun/releases/download/bun-v${bun_version}/${bun_archive}" \
    --output "/tmp/${bun_archive}"
echo "${bun_sha256}  /tmp/${bun_archive}" | sha256sum --check --strict
unzip -q "/tmp/${bun_archive}" -d /tmp/bun
install "/tmp/bun/bun-linux-${bun_target}/bun" /usr/local/bin/bun
ln --symbolic bun /usr/local/bin/bunx
rm --recursive /tmp/bun "/tmp/${bun_archive}"

rustup_version=$(manifest_value rustup version)
rustup_target=$(manifest_value rustup "${architecture}_target")
rustup_sha256=$(manifest_value rustup "${architecture}_sha256")
curl --fail --location --show-error \
    "https://static.rust-lang.org/rustup/archive/${rustup_version}/${rustup_target}/rustup-init" \
    --output /tmp/rustup-init
echo "${rustup_sha256}  /tmp/rustup-init" | sha256sum --check --strict
chmod +x /tmp/rustup-init
/tmp/rustup-init --no-modify-path --profile minimal --default-toolchain none -y
rm /tmp/rustup-init

stable=$(manifest_value rust stable)
nightly=$(manifest_value rust nightly)
rustup toolchain install "$stable" \
    --component clippy,llvm-tools-preview,rustfmt
rustup toolchain install "$nightly" \
    --component clippy,llvm-tools-preview,rust-src,rustfmt
rustup default "$stable"

binstall_version=$(manifest_value cargo_binstall version)
binstall_target=$(manifest_value cargo_binstall "${architecture}_target")
binstall_sha256=$(manifest_value cargo_binstall "${architecture}_sha256")
binstall_archive="cargo-binstall-${binstall_target}.tgz"
curl --fail --location --show-error \
    "https://github.com/cargo-bins/cargo-binstall/releases/download/v${binstall_version}/${binstall_archive}" \
    --output "/tmp/${binstall_archive}"
echo "${binstall_sha256}  /tmp/${binstall_archive}" | sha256sum --check --strict
tar --extract --gzip --file "/tmp/${binstall_archive}" \
    --directory /opt/cargo/bin
rm "/tmp/${binstall_archive}"
EOF

RUN --mount=type=cache,target=/opt/cargo/registry,sharing=locked \
    --mount=type=cache,target=/opt/cargo/git,sharing=locked <<'EOF'
set -eu

python3 - <<'PY' >/tmp/cargo-tools
import tomllib

with open("/usr/local/share/tact-dev/tools.toml", "rb") as manifest_file:
    manifest = tomllib.load(manifest_file)

for tool in manifest["tools"].values():
    print(f'{tool["package"]}@{tool["version"]}')
PY

while IFS= read -r package; do
    cargo binstall \
        --no-confirm \
        --force \
        --no-discover-github-token \
        --maximum-resolution-timeout 20 \
        --strategies quick-install,compile \
        "$package"
done </tmp/cargo-tools
rm /tmp/cargo-tools

# rustup proxies need their invoked names, while installed CLI binaries are
# ordinary files. Expose both from a system path before CARGO_HOME becomes the
# writable per-user cache at runtime.
cp --archive /opt/cargo/bin/. /usr/local/bin/
EOF

FROM toolchain AS tact-builder

WORKDIR /source
COPY . .
RUN --mount=type=cache,target=/opt/cargo/registry,sharing=locked \
    --mount=type=cache,target=/opt/cargo/git,sharing=locked \
    --mount=type=cache,target=/source/target,sharing=locked \
    cargo build --locked --release \
    && install target/release/tact /tmp/tact

FROM toolchain AS tact-dev

ARG TACT_UID=1000
ARG TACT_GID=1000

ENV HOME=/home/tact \
    CARGO_HOME=/home/tact/.cargo \
    RUSTUP_HOME=/usr/local/rustup \
    UV_CACHE_DIR=/home/tact/.cache/uv \
    NPM_CONFIG_CACHE=/home/tact/.npm \
    BUN_INSTALL_CACHE_DIR=/home/tact/.bun/install/cache \
    PATH=/home/tact/.cargo/bin:/home/tact/.local/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

COPY --from=tact-builder /tmp/tact /usr/local/bin/tact
COPY --chmod=0755 docker/dev/entrypoint.sh /usr/local/bin/tact-entrypoint

RUN groupadd --gid "$TACT_GID" tact \
    && useradd --uid "$TACT_UID" --gid tact --create-home --shell /bin/bash tact \
    && mkdir -p /workspace /home/tact/.cargo /home/tact/.cache/uv \
        /home/tact/.npm /home/tact/.bun/install/cache \
    && chown -R tact:tact /workspace /home/tact \
    && chmod 0777 /home/tact

USER tact
WORKDIR /workspace
ENTRYPOINT ["/usr/local/bin/tact-entrypoint"]