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
4 changes: 2 additions & 2 deletions .devcontainer/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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`).
8 changes: 6 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand All @@ -34,6 +37,7 @@
"vscode": {
"extensions": [
"EditorConfig.EditorConfig",
"julialang.language-julia",
"eamodio.gitlens",
"streetsidesoftware.code-spell-checker",
"timonwong.shellcheck",
Expand Down
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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/
60 changes: 60 additions & 0 deletions .github/workflows/container-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# 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

Check warning on line 32 in .github/workflows/container-build.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "read-all" with specific permissions (e.g., "contents: read").

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_statistease&issues=AZ9OkE6lOI8JwMZvgop6&open=AZ9OkE6lOI8JwMZvgop6&pullRequest=44

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
70 changes: 36 additions & 34 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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()"]
35 changes: 32 additions & 3 deletions QUICKSTART-MAINTAINER.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions QUICKSTART-USER.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Before you begin, ensure you have:

== Install

=== Option 1: Native Julia

[source,bash]
----
# Clone and set up
Expand All @@ -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]
Expand Down
Loading