diff --git a/.devcontainer/README.adoc b/.devcontainer/README.adoc index edf06be..ed9db42 100644 --- a/.devcontainer/README.adoc +++ b/.devcontainer/README.adoc @@ -5,7 +5,7 @@ == Overview -This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, and nickel automatically. +This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, nickel, and Julia 1.10 (via Juliaup) automatically. == VS Code (Local) @@ -25,4 +25,4 @@ This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, an == Customization -Replace `Statistikles` placeholders in both `devcontainer.json` and `Containerfile` with your actual project name. Run `just deps` to verify the environment after first launch. +Run `just setup` to verify the environment after first launch (already run automatically by `postCreateCommand`). diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 31230a8..efb1683 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -18,10 +18,13 @@ "version": "latest" }, "ghcr.io/jdx/devcontainer-features/just:1": {}, - "ghcr.io/nickel-lang/devcontainer-feature:0": {} + "ghcr.io/nickel-lang/devcontainer-feature:0": {}, + "ghcr.io/julialang/devcontainer-features/julia:1": { + "channel": "1.10" + } }, - "postCreateCommand": "just deps", + "postCreateCommand": "just setup", "remoteUser": "nonroot", @@ -34,6 +37,7 @@ "vscode": { "extensions": [ "EditorConfig.EditorConfig", + "julialang.language-julia", "eamodio.gitlens", "streetsidesoftware.code-spell-checker", "timonwong.shellcheck", diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7a46de5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: MPL-2.0 +# Build-context excludes for `docker build`/`podman build` (Containerfile, +# .devcontainer/Containerfile). Podman falls back to this file when no +# .containerignore is present. + +.git/ +.github/ +.devcontainer/ +docs/ +proofs/ +ffi/ +benches/ +examples/ +generated/ +.claude/ +node_modules/ +*.log +/tmp/ diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml new file mode 100644 index 0000000..7f154c7 --- /dev/null +++ b/.github/workflows/container-build.yml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Statistikles — Container Build +# +# Builds the root Containerfile so it cannot silently rot back into a +# template stub. Build only — no push, no registry login. + +name: Container Build + +on: + push: + branches: [main] + paths: + - 'Containerfile' + - '.dockerignore' + - 'Project.toml' + - 'Manifest.toml' + - 'src/**' + - '.github/workflows/container-build.yml' + pull_request: + branches: [main] + paths: + - 'Containerfile' + - '.dockerignore' + - 'Project.toml' + - 'Manifest.toml' + - 'src/**' + - '.github/workflows/container-build.yml' + workflow_dispatch: + +permissions: read-all + +concurrency: + group: container-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: docker build . + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Build Containerfile + run: docker build -f Containerfile -t statistikles:ci . + + - name: Smoke — run the entrypoint + run: | + # No LM Studio endpoint is reachable from the runner, so main() + # (src/tools/chat.jl) takes its documented offline-demo fallback + # and exits 0 — see the Containerfile's ENTRYPOINT comment and + # QUICKSTART-USER.adoc "First Run". + docker run --rm statistikles:ci | tee /tmp/container-run.log + grep -q "Statistikles v0.1.0" /tmp/container-run.log + grep -q "Cannot connect to LM Studio" /tmp/container-run.log + grep -q "All numbers above were computed by Julia, not an LLM." /tmp/container-run.log diff --git a/Containerfile b/Containerfile index 985834d..d9c0372 100644 --- a/Containerfile +++ b/Containerfile @@ -5,37 +5,39 @@ # Build: podman build -t statistikles:latest -f Containerfile . # Run: podman run --rm -it statistikles:latest # Seal: selur seal statistikles:latest - -# --- Build stage --- -FROM cgr.dev/chainguard/wolfi-base:latest AS build - -# TODO: Install build dependencies for your stack -# Examples: -# RUN apk add --no-cache rust cargo # Rust -# RUN apk add --no-cache elixir erlang # Elixir -# RUN apk add --no-cache zig # Zig - -WORKDIR /build -COPY . . - -# TODO: Replace with your build command -# Examples: -# RUN cargo build --release -# RUN mix deps.get && MIX_ENV=prod mix release -# RUN zig build -Doptimize=ReleaseSafe - -# --- Runtime stage --- -FROM cgr.dev/chainguard/static:latest - -# Copy built artifact from build stage -# TODO: Replace with your binary/artifact path -# Examples: -# COPY --from=build /build/target/release/statistikles /usr/local/bin/ -# COPY --from=build /build/_build/prod/rel/statistikles /app/ -# COPY --from=build /build/zig-out/bin/statistikles /usr/local/bin/ - -# Non-root user (chainguard images default to nonroot) -USER nonroot - -# TODO: Replace with your entrypoint -# ENTRYPOINT ["/usr/local/bin/statistikles"] +# +# Statistikles is a pure Julia package with no compiled binary artifact (see +# "Build from Source" in QUICKSTART-MAINTAINER.adoc) — the `julia` executable +# itself is the runtime, so there is nothing a separate build stage could +# hand off that the runtime doesn't also need. A single stage on the official +# Julia image is therefore correct here, not a shortcut. Chainguard ships no +# Julia package, so the org-default Wolfi/static base (docs/AI-CONVENTIONS.md) +# does not apply to this stack. + +# docker.io/library/julia:1.10 == Julia 1.10.11 (Debian trixie), matching the +# `julia_version` pinned in the committed Manifest.toml and .tool-versions. +FROM docker.io/library/julia:1.10@sha256:12477d07306333f0c59c04274196432a3834f8c44a0a10585de659fbf8ab0e54 + +# Non-root runtime user +RUN useradd --create-home --uid 1000 --shell /bin/bash statistikles +WORKDIR /app +COPY --chown=statistikles:statistikles . . +USER statistikles +ENV JULIA_DEPOT_PATH=/home/statistikles/.julia + +# Resolve the committed Manifest.toml exactly (no re-resolution) and +# precompile so the entrypoint starts promptly. +RUN julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()' + +# `main()` (src/tools/chat.jl) prints the startup banner, then tries to reach +# an LM Studio-compatible endpoint (STATISTIKLES_LM_URL, default +# http://localhost:1234/v1). With no LLM endpoint reachable from the +# container it prints "Cannot connect..." and runs the offline +# `run_examples()` demo instead, then exits 0 — it does not hang waiting for +# input. Run with `--network=host` to reach a host-side LM Studio at the +# default address for the interactive chat session instead. NOTE: setting +# STATISTIKLES_LM_URL at `docker/podman run` time has no effect — Julia's +# Pkg.precompile() above already baked the default URL into the package's +# precompile cache; to change it, set STATISTIKLES_LM_URL as an image build +# ENV before the RUN Pkg.precompile() line and rebuild. +ENTRYPOINT ["julia", "--project=/app", "-e", "using Statistikles; main()"] diff --git a/QUICKSTART-MAINTAINER.adoc b/QUICKSTART-MAINTAINER.adoc index cfb8612..56157a8 100644 --- a/QUICKSTART-MAINTAINER.adoc +++ b/QUICKSTART-MAINTAINER.adoc @@ -45,9 +45,38 @@ the full Julia dependency closure from source — the closure is instantiated on from the pinned `Manifest.toml` into a writable depot on first use (see the header comment in `guix.scm` for the rationale and the exact command). -The container (`Containerfile`) packaging surface exists in the repository but is -not yet functional — do not base a container distribution on it until it is -completed. +The `Containerfile` also builds a runnable image (see "Container" below). + +== Container + +[source,bash] +---- +podman build -t statistikles:latest -f Containerfile . +podman run --rm -it statistikles:latest +---- + +(`docker` works identically in place of `podman`.) The image is built from +the official `docker.io/library/julia:1.10` base (pinned by digest in the +Containerfile), runs as a non-root user, and its `ENTRYPOINT` is the same +`main()` entry point as `just run`. With no LLM endpoint reachable from the +container it falls back to the offline `run_examples()` demo and exits 0 — +see "First Run" in QUICKSTART-USER.adoc for the exact expected output. To +reach a host-side LM Studio instance at the default address instead: + +[source,bash] +---- +podman run --rm -it --network=host statistikles:latest +---- + +NOTE: `STATISTIKLES_LM_URL` set at `podman run` time has **no effect** on a +precompiled image or a precompiled native install — verified by testing both. +`Pkg.precompile()` evaluates and caches `BASE_URL` (`src/tools/lmstudio.jl`) +into the package's precompile image at the time it runs; loading the package +afterwards deserializes that cache rather than re-reading `ENV`. This is not +container-specific — the same applies to `just build && just run` natively. +To point a built image at a non-default endpoint, set `STATISTIKLES_LM_URL` +as a build-time `ENV` *before* the `Pkg.precompile()` step in the +Containerfile and rebuild. == Configuration diff --git a/QUICKSTART-USER.adoc b/QUICKSTART-USER.adoc index 87d3ba4..14c3599 100644 --- a/QUICKSTART-USER.adoc +++ b/QUICKSTART-USER.adoc @@ -37,6 +37,8 @@ Before you begin, ensure you have: == Install +=== Option 1: Native Julia + [source,bash] ---- # Clone and set up @@ -56,6 +58,23 @@ If you prefer not to use just, the equivalent raw command is: julia --project=. -e 'using Pkg; Pkg.instantiate()' ---- +=== Option 2: Container + +No local Julia install required — just Podman or Docker: + +[source,bash] +---- +git clone https://github.com/hyperpolymath/statistikles.git +cd statistikles +podman build -t statistikles:latest -f Containerfile . +podman run --rm -it statistikles:latest +---- + +(`docker` works identically in place of `podman`.) This builds the official +`docker.io/library/julia:1.10` base image with the project instantiated and +precompiled, then runs it as a non-root user. Expected output matches +"First Run" below. + == First Run [source,bash]