mirror of
https://github.com/logos-co/logos-package.git
synced 2026-08-27 10:11:10 +00:00
@@ -0,0 +1,228 @@
|
||||
name: lgx Doc-Tests
|
||||
|
||||
# Runs the executable lgx doc-tests (doctests/*.test.yaml) end-to-end via the
|
||||
# shared doctest CLI: each spec builds lgx from the commit under test, then
|
||||
# creates / adds / verifies / extracts / merges / signs .lgx packages and asserts
|
||||
# on lgx's output. If the docs drift from the tool, this job fails.
|
||||
#
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# One-time setup required for the clickable report links to work:
|
||||
#
|
||||
# 1. Repo Settings → Pages → "Build and deployment" → Source: "Deploy from a
|
||||
# branch", Branch: `gh-pages` / `(root)`. (The publish-report job creates
|
||||
# the gh-pages branch on its first run.)
|
||||
# 2. Nothing else — GITHUB_TOKEN already has the permissions granted below.
|
||||
#
|
||||
# Each run publishes the HTML reports to:
|
||||
# https://<owner>.github.io/<repo>/pr-<N>/<os>/ (pull requests)
|
||||
# https://<owner>.github.io/<repo>/main/<os>/ (pushes to main/master)
|
||||
# and (for PRs) posts/updates a comment with the links.
|
||||
#
|
||||
# Note: pull requests opened from forks get a read-only GITHUB_TOKEN, so the
|
||||
# Pages push and PR comment are skipped for them — the downloadable artifact is
|
||||
# still produced. PRs from branches in this repo get the full clickable links.
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
push:
|
||||
branches: [master, main]
|
||||
|
||||
concurrency:
|
||||
group: doctests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
doctests:
|
||||
name: Run lgx doc-tests (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Setup Cachix
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: logos-co
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
# Resolve the commit under test. For pull requests this is the PR's head
|
||||
# commit (not the synthetic merge commit); for pushes it's the pushed
|
||||
# commit. Passed to --release-for below so the doc-tests build THIS commit
|
||||
# of logos-package instead of the latest published flake.
|
||||
#
|
||||
# Fork PRs are the exception: their head commit lives in the fork, not in
|
||||
# logos-co/logos-package, so nix could not fetch
|
||||
# `github:logos-co/logos-package/<sha>`. We blank the SHA for forks
|
||||
# (--release-for repo= → pins that repo to latest), so the doc-tests still
|
||||
# run for fork PRs, just against master as before.
|
||||
- name: Resolve commit under test
|
||||
id: commit
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && \
|
||||
[ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||
echo "sha=" >> "$GITHUB_OUTPUT"
|
||||
echo "Fork PR detected — doc-tests will run against latest master."
|
||||
else
|
||||
echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# The runner is the shared `doctest` CLI, invoked directly via its flake
|
||||
# (github:logos-co/logos-doctest). The flake bundles Python + PyYAML, so no
|
||||
# pip install step is needed.
|
||||
#
|
||||
# All specs are passed to a SINGLE `doctest run` so they land in one HTML
|
||||
# report — the report's top-right dropdown switches between them. (Running
|
||||
# one spec per invocation would emit a separate file each and lose the
|
||||
# dropdown.) --continue-on-fail makes the run walk every spec/step so the
|
||||
# report is complete; the job still exits non-zero if any step failed.
|
||||
#
|
||||
# --release-for pins the {release} placeholder for this one repo to the
|
||||
# commit under test, so `github:logos-co/logos-package{release}` in each
|
||||
# spec becomes `.../<sha>` — the doc-tests exercise this PR/push rather
|
||||
# than master. An empty sha (fork PR) leaves the URL on latest.
|
||||
- name: Run every doc-test
|
||||
run: |
|
||||
mkdir -p "${{ runner.temp }}/reports"
|
||||
nix run github:logos-co/logos-doctest -- run \
|
||||
doctests/*.test.yaml \
|
||||
--verbose \
|
||||
--continue-on-fail \
|
||||
--release-for "logos-package=${{ steps.commit.outputs.sha }}" \
|
||||
--report "${{ runner.temp }}/reports/index.html"
|
||||
|
||||
- name: Verify markdown generation works
|
||||
run: |
|
||||
for spec in doctests/*.test.yaml; do
|
||||
name="$(basename "${spec%.test.yaml}")"
|
||||
nix run github:logos-co/logos-doctest -- generate "$spec" \
|
||||
--release-for "logos-package=${{ steps.commit.outputs.sha }}" \
|
||||
-o "/tmp/${name}.md"
|
||||
test -s "/tmp/${name}.md"
|
||||
echo "Generated /tmp/${name}.md ($(wc -l < "/tmp/${name}.md") lines)"
|
||||
done
|
||||
|
||||
- name: Upload execution reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lgx-doctest-reports-${{ matrix.os }}
|
||||
path: ${{ runner.temp }}/reports/
|
||||
if-no-files-found: warn
|
||||
|
||||
publish-report:
|
||||
name: Publish reports to GitHub Pages
|
||||
needs: doctests
|
||||
# Run even when tests fail — a failing run is exactly when you want to open
|
||||
# the report. Skip on forks, where GITHUB_TOKEN can't push or comment.
|
||||
if: ${{ always() && github.event.pull_request.head.repo.fork != true }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write # push to the gh-pages branch
|
||||
pull-requests: write # post/update the PR comment
|
||||
|
||||
# Serialize Pages pushes so two refs can't race on the gh-pages branch.
|
||||
concurrency:
|
||||
group: gh-pages-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Download all reports
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
# No `name:` → downloads every artifact into artifacts/<name>/...
|
||||
|
||||
- name: Arrange site directory
|
||||
id: arrange
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="pr-${{ github.event.pull_request.number }}"
|
||||
else
|
||||
BASE="main"
|
||||
fi
|
||||
echo "base=$BASE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mkdir -p "site/$BASE"
|
||||
found=""
|
||||
for os in ubuntu-latest macos-latest; do
|
||||
src="artifacts/lgx-doctest-reports-$os"
|
||||
# Each OS produces a single combined report (index.html) whose
|
||||
# built-in dropdown switches between specs — just publish it as-is.
|
||||
if [ -f "$src/index.html" ]; then
|
||||
mkdir -p "site/$BASE/$os"
|
||||
cp "$src/index.html" "site/$BASE/$os/index.html"
|
||||
found="$found $os"
|
||||
fi
|
||||
done
|
||||
echo "found=$found" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Top-level landing page for this ref linking to each OS.
|
||||
{
|
||||
echo "<!doctype html><meta charset=utf-8>"
|
||||
echo "<title>lgx doc-test reports — $BASE</title>"
|
||||
echo "<style>body{font:16px system-ui;margin:40px;max-width:640px}a{color:#2563eb}</style>"
|
||||
echo "<h1>lgx doc-test reports</h1>"
|
||||
echo "<p><strong>$BASE</strong> · commit <code>${GITHUB_SHA::7}</code></p><ul>"
|
||||
for os in ubuntu-latest macos-latest; do
|
||||
if [ -d "site/$BASE/$os" ]; then
|
||||
echo "<li><a href=\"./$os/\">$os</a></li>"
|
||||
fi
|
||||
done
|
||||
echo "</ul>"
|
||||
} > "site/$BASE/index.html"
|
||||
|
||||
- name: Deploy to gh-pages
|
||||
if: steps.arrange.outputs.found != ''
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./site
|
||||
keep_files: true # don't wipe other PRs' directories
|
||||
commit_message: "Publish lgx doc-test reports for ${{ steps.arrange.outputs.base }} (${{ github.sha }})"
|
||||
|
||||
- name: Comment on PR with report links
|
||||
if: ${{ github.event_name == 'pull_request' && steps.arrange.outputs.found != '' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const base = "${{ steps.arrange.outputs.base }}";
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const root = `https://${owner}.github.io/${repo}/${base}`;
|
||||
const oses = "${{ steps.arrange.outputs.found }}".trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
const links = oses.map(os => `- [\`${os}\` reports](${root}/${os}/)`).join("\n");
|
||||
const marker = "<!-- lgx-doctest-report-links -->";
|
||||
const body =
|
||||
`${marker}\n` +
|
||||
`### 📊 lgx doc-test reports\n\n` +
|
||||
`\`lgx\` built from this commit, packaging and signing real \`.lgx\` files — ` +
|
||||
`rendered docs alongside the commands actually run and their output ` +
|
||||
`(updated each run, commit \`${context.sha.slice(0,7)}\`):\n\n` +
|
||||
`${links}\n\n` +
|
||||
`_Pages can take a minute to update after the run finishes._`;
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner, repo, issue_number: context.issue.number, per_page: 100,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: context.issue.number, body });
|
||||
}
|
||||
+12
@@ -2,4 +2,16 @@ build/
|
||||
.DS_Store
|
||||
Testing/
|
||||
result
|
||||
result-*
|
||||
.idea/
|
||||
|
||||
# doctests/run.sh local-run artifacts (the specs normally run in a temp dir,
|
||||
# but these can appear if you run the lgx commands by hand in the repo).
|
||||
lgx
|
||||
keys/
|
||||
trusted/
|
||||
extracted/
|
||||
variant
|
||||
ext
|
||||
did.txt
|
||||
*.lgx
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
name: "Packaging a Module with lgx"
|
||||
output: lgx-cli.md
|
||||
release: ""
|
||||
|
||||
intro: |
|
||||
[`lgx`](https://github.com/logos-co/logos-package) is the Logos **package tool**.
|
||||
An `.lgx` file is just a **gzipped tar archive** with a `manifest.json` at its root
|
||||
and one or more platform payloads under `variants/<platform>/`. `lgx` is the CLI that
|
||||
builds, inspects, extracts, merges, and (in the companion
|
||||
[signing doc-test](lgx-signing.md)) signs those archives — a self-contained C++ tool
|
||||
with no Qt and no module runtime behind it.
|
||||
|
||||
This doc-test builds **this** `lgx` commit and drives the full packaging lifecycle:
|
||||
|
||||
1. Build the `lgx` CLI from this repository's flake.
|
||||
2. `create` an empty package and read its `manifest`.
|
||||
3. `add` this machine's platform variant and `verify` the result.
|
||||
4. `extract` a variant back out, and inspect the raw archive with `tar`.
|
||||
5. `merge` two single-variant packages into one multi-variant `.lgx`.
|
||||
|
||||
Every command is the real binary built from the commit under test, so a green run is
|
||||
evidence that this change keeps the packaging workflow working end-to-end.
|
||||
|
||||
what_you_build: "A `greeter.lgx` package built, inspected, and extracted with `lgx`, plus a multi-variant `widget.lgx` assembled with `lgx merge`."
|
||||
|
||||
what_you_learn:
|
||||
- What an `.lgx` package is on disk (a gzipped tar of a manifest + a `variants/` tree)
|
||||
- How to create a package and read its manifest with `lgx create` / `lgx manifest`
|
||||
- How to add a platform variant, verify structural validity, and extract it back out
|
||||
- How to merge per-platform single-variant packages into one multi-variant `.lgx`
|
||||
|
||||
prerequisites:
|
||||
- |
|
||||
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
||||
- "A Linux or macOS machine."
|
||||
|
||||
sections:
|
||||
- title: "Build the lgx CLI"
|
||||
step: true
|
||||
text: |
|
||||
`lgx` ships as a C++ CLI. Build it straight from the flake's `#lgx` output and
|
||||
link the result as `./lgx`, so the binary lands at `./lgx/bin/lgx`.
|
||||
|
||||
> The `{release}` in the URL is what pins the build to a specific commit: the
|
||||
> doc-test runner expands it to a concrete ref. Locally that is this checkout's
|
||||
> `HEAD` (see `run.sh`); in CI it is the commit being tested. With no pin it falls
|
||||
> back to the latest `master`. Developing against a local checkout? Replace the
|
||||
> GitHub reference with `.`, e.g. `nix build '.#lgx' -o lgx`.
|
||||
steps:
|
||||
- title: "Build lgx"
|
||||
run: "nix build 'github:logos-co/logos-package{release}#lgx' -o lgx"
|
||||
code_block: |
|
||||
# From inside the clone this is simply: nix build '.#lgx' -o lgx
|
||||
nix build 'github:logos-co/logos-package{release}#lgx' -o lgx
|
||||
post_text: "The `-o lgx` flag names the result symlink, so the executable is at `./lgx/bin/lgx`."
|
||||
|
||||
- title: "Confirm it runs"
|
||||
run: "./lgx/bin/lgx --version"
|
||||
expect_contains:
|
||||
- "lgx version"
|
||||
check_file: "lgx/bin/lgx"
|
||||
|
||||
- title: "Detect the platform variant"
|
||||
step: true
|
||||
text: |
|
||||
An `.lgx` package carries one or more **platform variants** — `linux-x86_64`,
|
||||
`darwin-arm64`, and so on — each holding the payload for that platform. Detect this
|
||||
machine's variant once (and the matching shared-library extension, `.so` on Linux /
|
||||
`.dylib` on macOS) and stash both in files so the packaging steps below can reuse
|
||||
them.
|
||||
steps:
|
||||
- title: "Write the variant and library extension to ./variant and ./ext"
|
||||
run: |
|
||||
case "$(uname -s) $(uname -m)" in
|
||||
"Linux x86_64") echo linux-x86_64 > variant; echo so > ext ;;
|
||||
"Linux aarch64") echo linux-arm64 > variant; echo so > ext ;;
|
||||
"Darwin arm64") echo darwin-arm64 > variant; echo dylib > ext ;;
|
||||
"Darwin x86_64") echo darwin-x86_64 > variant; echo dylib > ext ;;
|
||||
*) echo "unsupported platform: $(uname -sm)" >&2; exit 1 ;;
|
||||
esac
|
||||
echo "variant: $(cat variant) ext: $(cat ext)"
|
||||
expect_contains:
|
||||
- "variant:"
|
||||
|
||||
- title: "Create a package"
|
||||
step: true
|
||||
text: |
|
||||
`lgx create <name>` writes an empty skeleton package — a `manifest.json` with
|
||||
sensible defaults plus an empty `variants/` directory — to `<name>.lgx`.
|
||||
steps:
|
||||
- title: "Create greeter.lgx"
|
||||
run: "./lgx/bin/lgx create greeter"
|
||||
expect_contains:
|
||||
- "Created package: greeter.lgx"
|
||||
check_file: "greeter.lgx"
|
||||
|
||||
- title: "Read the fresh manifest"
|
||||
text: |
|
||||
`lgx manifest` prints the embedded `manifest.json` in human-readable form. A
|
||||
freshly created package starts at version `0.0.1`, manifest schema `0.3.0`, with
|
||||
no type, no variants, and no signature yet.
|
||||
run: "./lgx/bin/lgx manifest greeter.lgx"
|
||||
code_block: "lgx manifest greeter.lgx"
|
||||
expect_contains:
|
||||
- "greeter"
|
||||
- "0.0.1"
|
||||
- "Manifest ver.:"
|
||||
- "0.3.0"
|
||||
- "Variants: (none)"
|
||||
- "Signed: no"
|
||||
|
||||
- title: "Add a platform variant"
|
||||
step: true
|
||||
text: |
|
||||
`lgx add` drops a payload into the package under `variants/<variant>/` and records
|
||||
the entry point in the manifest's `main` map. We add a stub shared library for this
|
||||
machine's variant. (In a real build the payload is the module's compiled `.so` /
|
||||
`.dylib`; `lgx` does not load it — it just packages it.)
|
||||
steps:
|
||||
- title: "Add this machine's variant"
|
||||
run: |
|
||||
V="$(cat variant)"; E="$(cat ext)"
|
||||
echo 'stub library' > "libgreeter.$E"
|
||||
./lgx/bin/lgx add greeter.lgx --variant "$V" --files "libgreeter.$E"
|
||||
code_block: |
|
||||
# On Linux x86_64 this expands to:
|
||||
echo 'stub library' > libgreeter.so
|
||||
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
|
||||
expect_contains:
|
||||
- "Added variant"
|
||||
- "to greeter.lgx"
|
||||
post_text: "Confirm the archive layout — a `manifest.json` beside the `variants/` tree:"
|
||||
extra_run:
|
||||
run: "tar -tzf greeter.lgx"
|
||||
post_text: |
|
||||
```
|
||||
manifest.json
|
||||
variants/
|
||||
variants/<platform>/
|
||||
variants/<platform>/libgreeter.so # .dylib on macOS
|
||||
```
|
||||
|
||||
- title: "See the variant in the manifest"
|
||||
text: |
|
||||
The manifest now lists the variant and points its `main` entry at the file we
|
||||
added. A content hash (`Root hash`) is recomputed on every save.
|
||||
run: "./lgx/bin/lgx manifest greeter.lgx"
|
||||
code_block: "lgx manifest greeter.lgx"
|
||||
expect_contains:
|
||||
- "Root hash:"
|
||||
- "Variants:"
|
||||
- "libgreeter."
|
||||
|
||||
- title: "Verify the package"
|
||||
step: true
|
||||
text: |
|
||||
`lgx verify` validates a package against the LGX spec — it recomputes the content
|
||||
hashes (the Merkle tree) and checks them against the manifest, then reports the
|
||||
signature state. Our package is unsigned, so verification passes and notes it is
|
||||
unsigned. The exit code is `0` on success, so `verify` is safe to gate CI on.
|
||||
steps:
|
||||
- title: "Verify greeter.lgx"
|
||||
run: "./lgx/bin/lgx verify greeter.lgx"
|
||||
code_block: "lgx verify greeter.lgx"
|
||||
expect_contains:
|
||||
- "Package structure is valid: greeter.lgx"
|
||||
- "Package is unsigned"
|
||||
|
||||
- title: "Extract a variant"
|
||||
step: true
|
||||
text: |
|
||||
`lgx extract` unpacks a variant's payload back onto disk — the inverse of `add`.
|
||||
Files land under `<output>/<variant>/`.
|
||||
steps:
|
||||
- title: "Extract this machine's variant"
|
||||
run: |
|
||||
V="$(cat variant)"
|
||||
./lgx/bin/lgx extract greeter.lgx --variant "$V" --output ./extracted
|
||||
code_block: |
|
||||
lgx extract greeter.lgx --variant linux-x86_64 --output ./extracted
|
||||
expect_contains:
|
||||
- "Extracted variant"
|
||||
- "./extracted"
|
||||
post_text: "The payload is now on disk under `./extracted/<variant>/`:"
|
||||
extra_run:
|
||||
run: |
|
||||
V="$(cat variant)"
|
||||
ls "./extracted/$V"
|
||||
code_block: "ls ./extracted/<variant>"
|
||||
|
||||
- title: "Merge per-platform packages"
|
||||
step: true
|
||||
text: |
|
||||
Real releases ship one `.lgx` covering every platform. A common pattern is to build
|
||||
a **single-variant** package on each platform's CI runner, then `lgx merge` them
|
||||
into one multi-variant archive. The inputs must share identical metadata (only the
|
||||
variant-specific `main` entries differ). We simulate it here by hand-building a
|
||||
Linux and a macOS package for the same module and merging them.
|
||||
steps:
|
||||
- title: "Build two single-variant packages"
|
||||
run: |
|
||||
# A Linux-only package...
|
||||
./lgx/bin/lgx create widget && mv widget.lgx widget-linux.lgx
|
||||
echo 'linux stub' > libwidget.so
|
||||
./lgx/bin/lgx add widget-linux.lgx --variant linux-x86_64 --files libwidget.so
|
||||
|
||||
# ...and a macOS-only package for the same module.
|
||||
./lgx/bin/lgx create widget && mv widget.lgx widget-darwin.lgx
|
||||
echo 'darwin stub' > libwidget.dylib
|
||||
./lgx/bin/lgx add widget-darwin.lgx --variant darwin-arm64 --files libwidget.dylib
|
||||
expect_contains:
|
||||
- "Added variant"
|
||||
|
||||
- title: "Merge them into one .lgx"
|
||||
text: |
|
||||
`lgx merge` writes a single package carrying both variants. The variants are
|
||||
listed alphabetically in the summary.
|
||||
run: "./lgx/bin/lgx merge widget-linux.lgx widget-darwin.lgx -o widget.lgx"
|
||||
code_block: "lgx merge widget-linux.lgx widget-darwin.lgx -o widget.lgx"
|
||||
expect_contains:
|
||||
- "Merged 2 packages into widget.lgx (darwin-arm64, linux-x86_64)"
|
||||
|
||||
- title: "Confirm both variants are present"
|
||||
text: "The merged manifest carries both platforms, and the package verifies:"
|
||||
run: "./lgx/bin/lgx manifest widget.lgx"
|
||||
code_block: "lgx manifest widget.lgx"
|
||||
expect_contains:
|
||||
- "darwin-arm64"
|
||||
- "linux-x86_64"
|
||||
|
||||
- run: "./lgx/bin/lgx verify widget.lgx"
|
||||
code_block: "lgx verify widget.lgx"
|
||||
expect_contains:
|
||||
- "Package structure is valid: widget.lgx"
|
||||
|
||||
- title: "Recap"
|
||||
text: |
|
||||
You built `lgx` from this commit and ran a full packaging lifecycle — creating a
|
||||
package, adding a platform variant, verifying it, extracting it, and merging
|
||||
per-platform packages into one multi-variant `.lgx`:
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `create <name>` | Write an empty skeleton `<name>.lgx` |
|
||||
| `add <pkg> -v <variant> -f <path>` | Add a payload for a variant (records `main`) |
|
||||
| `manifest <pkg> [--json]` | Print the embedded `manifest.json` (`--json` for raw bytes) |
|
||||
| `verify <pkg>` | Validate structure + content hashes, report signature state |
|
||||
| `extract <pkg> -v <variant> -o <dir>` | Unpack a variant's payload to disk |
|
||||
| `merge <pkg…> -o <out>` | Combine single-variant packages into one multi-variant `.lgx` |
|
||||
|
||||
Signing, signature inspection, and trusted-key management are covered in the
|
||||
companion [signing doc-test](lgx-signing.md).
|
||||
@@ -0,0 +1,230 @@
|
||||
name: "Signing and Trusting an lgx Package"
|
||||
output: lgx-signing.md
|
||||
release: ""
|
||||
|
||||
intro: |
|
||||
[`lgx`](https://github.com/logos-co/logos-package) packages can be **signed** so
|
||||
consumers can verify who produced them. Signing uses an **Ed25519** keypair whose public
|
||||
half is published as a [`did:jwk:`](https://github.com/quartzjer/did-jwk/blob/main/spec.md)
|
||||
decentralized identifier; `lgx sign` writes a `manifest.sig` next to the manifest, and
|
||||
`lgx verify` checks it. A **keyring** of trusted DIDs lets `verify` go one step further
|
||||
and report whether the signer is one you trust.
|
||||
|
||||
This doc-test builds **this** `lgx` commit and runs the full signing-and-trust lifecycle:
|
||||
|
||||
1. Build `lgx` and create a small package to sign.
|
||||
2. `keygen` an Ed25519 keypair (printing its `did:jwk:` DID).
|
||||
3. `sign` the package and inspect the signature with `manifest` and `signature`.
|
||||
4. `verify` it — first as an *untrusted* signer, then after adding the DID to a keyring.
|
||||
|
||||
Every key directory is passed explicitly (`--output-dir`, `--keys-dir`, `--keyring-dir`)
|
||||
so the whole flow is self-contained and never touches your real `~/.config/logos`.
|
||||
|
||||
what_you_build: "A signed `greeter.lgx`, an Ed25519 signing key, and a trusted-key keyring that makes `lgx verify` recognise the signer."
|
||||
|
||||
what_you_learn:
|
||||
- How to generate an Ed25519 signing keypair and its `did:jwk:` DID with `lgx keygen`
|
||||
- How to sign a package and read back its signature with `lgx sign` / `lgx signature`
|
||||
- How `lgx verify` reports signature validity and distinguishes trusted from untrusted signers
|
||||
- How to manage trusted DIDs with `lgx keyring add` / `list`
|
||||
|
||||
prerequisites:
|
||||
- |
|
||||
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
||||
- "A Linux or macOS machine."
|
||||
|
||||
sections:
|
||||
- title: "Build lgx and create a package to sign"
|
||||
step: true
|
||||
text: |
|
||||
Build the `lgx` CLI from this commit, then create a one-variant package — the thing
|
||||
we will sign. (The [packaging doc-test](lgx-cli.md) covers `create` / `add` in
|
||||
detail; here they are just setup.)
|
||||
|
||||
> The `{release}` placeholder pins the build to the commit under test (this
|
||||
> checkout's `HEAD` locally, the PR/push commit in CI). Against a local checkout use
|
||||
> `nix build '.#lgx' -o lgx`.
|
||||
steps:
|
||||
- title: "Build lgx"
|
||||
run: "nix build 'github:logos-co/logos-package{release}#lgx' -o lgx"
|
||||
code_block: |
|
||||
# From inside the clone this is simply: nix build '.#lgx' -o lgx
|
||||
nix build 'github:logos-co/logos-package{release}#lgx' -o lgx
|
||||
check_file: "lgx/bin/lgx"
|
||||
|
||||
- title: "Create and populate greeter.lgx"
|
||||
text: |
|
||||
Detect this machine's variant (and shared-library extension), create the
|
||||
package, and add a stub payload so there is content to sign.
|
||||
run: |
|
||||
case "$(uname -s) $(uname -m)" in
|
||||
"Linux x86_64") V=linux-x86_64; E=so ;;
|
||||
"Linux aarch64") V=linux-arm64; E=so ;;
|
||||
"Darwin arm64") V=darwin-arm64; E=dylib ;;
|
||||
"Darwin x86_64") V=darwin-x86_64; E=dylib ;;
|
||||
*) echo "unsupported platform: $(uname -sm)" >&2; exit 1 ;;
|
||||
esac
|
||||
./lgx/bin/lgx create greeter
|
||||
echo 'stub library' > "libgreeter.$E"
|
||||
./lgx/bin/lgx add greeter.lgx --variant "$V" --files "libgreeter.$E"
|
||||
code_block: |
|
||||
lgx create greeter
|
||||
echo 'stub library' > libgreeter.so # .dylib on macOS
|
||||
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
|
||||
expect_contains:
|
||||
- "Added variant"
|
||||
check_file: "greeter.lgx"
|
||||
|
||||
- title: "Generate a signing key"
|
||||
step: true
|
||||
text: |
|
||||
`lgx keygen` creates an Ed25519 keypair and writes three files — the secret key
|
||||
(`.jwk`), the public key (`.pub`), and the DID string (`.did`). It prints the
|
||||
`did:jwk:` DID to stdout. We point `--output-dir` at a local `./keys` directory and
|
||||
stash the DID in a file so later steps can refer to it.
|
||||
steps:
|
||||
- title: "Run keygen into ./keys"
|
||||
text: |
|
||||
`keygen` prints the DID to stdout and also writes it to `./keys/demo-key.did`. We
|
||||
copy that file to `./did.txt` so later steps can pass the exact DID without
|
||||
re-deriving it.
|
||||
run: |
|
||||
./lgx/bin/lgx keygen --name demo-key --output-dir ./keys
|
||||
cp ./keys/demo-key.did did.txt
|
||||
code_block: |
|
||||
lgx keygen --name demo-key --output-dir ./keys
|
||||
# → did:jwk:eyJjcnYiOiJFZDI1NTE5Ii... (also saved to ./keys/demo-key.did)
|
||||
expect_contains:
|
||||
- "did:jwk:"
|
||||
- "Keypair generated:"
|
||||
- "demo-key.{jwk,pub,did}"
|
||||
check_file: "keys/demo-key.jwk"
|
||||
post_text: |
|
||||
The keypair now lives under `./keys/` as `demo-key.{jwk,pub,did}`. The `.jwk`
|
||||
file is the **secret** key — keep it safe; only the DID is meant to be shared.
|
||||
|
||||
- title: "Sign the package"
|
||||
step: true
|
||||
text: |
|
||||
`lgx sign` validates the package, then writes a `manifest.sig` containing the
|
||||
signer's DID, an Ed25519 signature over the manifest bytes, and optional
|
||||
self-asserted signer metadata (`--name` / `--url`). We pass `--keys-dir ./keys` so it
|
||||
finds the key we just generated.
|
||||
steps:
|
||||
- title: "Sign greeter.lgx"
|
||||
run: |
|
||||
./lgx/bin/lgx sign greeter.lgx --key demo-key --keys-dir ./keys \
|
||||
--name "Logos Demo" --url "https://logos.co"
|
||||
code_block: |
|
||||
lgx sign greeter.lgx --key demo-key --keys-dir ./keys \
|
||||
--name "Logos Demo" --url "https://logos.co"
|
||||
expect_contains:
|
||||
- "Package signed: greeter.lgx"
|
||||
- "Signer DID:"
|
||||
|
||||
- title: "See the signature in the manifest"
|
||||
text: |
|
||||
`lgx manifest` now reports the package as signed, along with the signer's DID and
|
||||
self-asserted name/URL.
|
||||
run: "./lgx/bin/lgx manifest greeter.lgx"
|
||||
code_block: "lgx manifest greeter.lgx"
|
||||
expect_contains:
|
||||
- "Signed: yes"
|
||||
- "Signer DID:"
|
||||
- "Logos Demo (self-asserted)"
|
||||
|
||||
- title: "Read the raw signature"
|
||||
text: |
|
||||
`lgx signature` prints the raw `manifest.sig` JSON — byte-identical to the file
|
||||
inside the `.lgx`. It carries the algorithm, the signer DID, the base64
|
||||
signature, and the signer metadata. Unsigned packages print nothing and exit 0,
|
||||
so tooling tells "unsigned" from "error" by the exit status, not the output.
|
||||
run: "./lgx/bin/lgx signature greeter.lgx"
|
||||
code_block: "lgx signature greeter.lgx"
|
||||
expect_contains:
|
||||
- '"algorithm": "ed25519"'
|
||||
- '"did":'
|
||||
- '"signature":'
|
||||
|
||||
- title: "Verify an untrusted signer"
|
||||
step: true
|
||||
text: |
|
||||
`lgx verify` always checks structure and (for signed packages) the signature itself.
|
||||
Trust is separate: with no matching entry in the keyring, the signature is reported
|
||||
**valid** but the signer is flagged as **not** trusted. We point `--keyring-dir` at an
|
||||
empty `./trusted` directory to make that explicit.
|
||||
steps:
|
||||
- title: "Verify against an empty keyring"
|
||||
run: |
|
||||
mkdir -p trusted
|
||||
./lgx/bin/lgx verify greeter.lgx --keyring-dir ./trusted
|
||||
code_block: |
|
||||
mkdir -p trusted
|
||||
lgx verify greeter.lgx --keyring-dir ./trusted
|
||||
expect_contains:
|
||||
- "Package structure is valid: greeter.lgx"
|
||||
- "Signature is valid"
|
||||
- "Signer DID is NOT in trusted keyring"
|
||||
|
||||
- title: "Trust the signer"
|
||||
step: true
|
||||
text: |
|
||||
`lgx keyring add` records a DID as trusted under a short name. We add the DID we
|
||||
stashed earlier (still in `did.txt`) to the `./trusted` keyring, then list it.
|
||||
steps:
|
||||
- title: "Add the DID to the keyring"
|
||||
run: |
|
||||
./lgx/bin/lgx keyring add logos-demo "$(cat did.txt)" \
|
||||
--display-name "Logos Demo" --url "https://logos.co" --dir ./trusted
|
||||
code_block: |
|
||||
lgx keyring add logos-demo "$DID" \
|
||||
--display-name "Logos Demo" --url "https://logos.co" --dir ./trusted
|
||||
expect_contains:
|
||||
- "Added trusted key: logos-demo"
|
||||
- "DID: did:jwk:"
|
||||
|
||||
- title: "List trusted keys"
|
||||
run: "./lgx/bin/lgx keyring list --dir ./trusted"
|
||||
code_block: "lgx keyring list --dir ./trusted"
|
||||
expect_contains:
|
||||
- "logos-demo (Logos Demo)"
|
||||
- "DID: did:jwk:"
|
||||
|
||||
- title: "Verify a trusted signer"
|
||||
step: true
|
||||
text: |
|
||||
Re-run `verify` against the populated keyring. Now that the signer's DID is trusted,
|
||||
`verify` adds a final line naming the trusted key — the same valid signature, now
|
||||
recognised.
|
||||
steps:
|
||||
- title: "Verify against the populated keyring"
|
||||
run: "./lgx/bin/lgx verify greeter.lgx --keyring-dir ./trusted"
|
||||
code_block: "lgx verify greeter.lgx --keyring-dir ./trusted"
|
||||
expect_contains:
|
||||
- "Signature is valid"
|
||||
- "Signer is trusted: logos-demo"
|
||||
|
||||
- title: "Recap"
|
||||
text: |
|
||||
You built `lgx` from this commit and ran the full signing-and-trust lifecycle —
|
||||
generating a key, signing a package, inspecting the signature, and verifying it both
|
||||
as an untrusted and a trusted signer:
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `keygen --name <n> --output-dir <dir>` | Generate an Ed25519 keypair; print its `did:jwk:` DID |
|
||||
| `sign <pkg> --key <n> --keys-dir <dir> [--name …] [--url …]` | Write `manifest.sig` with the signer's DID |
|
||||
| `signature <pkg>` | Print the raw `manifest.sig` JSON (unsigned → empty + exit 0) |
|
||||
| `verify <pkg> [--keyring-dir <dir>]` | Validate structure + signature; report trust |
|
||||
| `keyring add <name> <did> [--dir <dir>]` | Trust a DID under a short name |
|
||||
| `keyring list [--dir <dir>]` | List trusted DIDs |
|
||||
|
||||
Creating, adding variants, extracting, and merging packages are covered in the
|
||||
companion [packaging doc-test](lgx-cli.md).
|
||||
@@ -0,0 +1,255 @@
|
||||
# Packaging a Module with lgx
|
||||
|
||||
[`lgx`](https://github.com/logos-co/logos-package) is the Logos **package tool**.
|
||||
An `.lgx` file is just a **gzipped tar archive** with a `manifest.json` at its root
|
||||
and one or more platform payloads under `variants/<platform>/`. `lgx` is the CLI that
|
||||
builds, inspects, extracts, merges, and (in the companion
|
||||
[signing doc-test](lgx-signing.md)) signs those archives — a self-contained C++ tool
|
||||
with no Qt and no module runtime behind it.
|
||||
|
||||
This doc-test builds **this** `lgx` commit and drives the full packaging lifecycle:
|
||||
|
||||
1. Build the `lgx` CLI from this repository's flake.
|
||||
2. `create` an empty package and read its `manifest`.
|
||||
3. `add` this machine's platform variant and `verify` the result.
|
||||
4. `extract` a variant back out, and inspect the raw archive with `tar`.
|
||||
5. `merge` two single-variant packages into one multi-variant `.lgx`.
|
||||
|
||||
Every command is the real binary built from the commit under test, so a green run is
|
||||
evidence that this change keeps the packaging workflow working end-to-end.
|
||||
|
||||
**What you'll build:** A `greeter.lgx` package built, inspected, and extracted with `lgx`, plus a multi-variant `widget.lgx` assembled with `lgx merge`.
|
||||
|
||||
**What you'll learn:**
|
||||
|
||||
- What an `.lgx` package is on disk (a gzipped tar of a manifest + a `variants/` tree)
|
||||
- How to create a package and read its manifest with `lgx create` / `lgx manifest`
|
||||
- How to add a platform variant, verify structural validity, and extract it back out
|
||||
- How to merge per-platform single-variant packages into one multi-variant `.lgx`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
||||
|
||||
- A Linux or macOS machine.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Build the lgx CLI
|
||||
|
||||
`lgx` ships as a C++ CLI. Build it straight from the flake's `#lgx` output and
|
||||
link the result as `./lgx`, so the binary lands at `./lgx/bin/lgx`.
|
||||
|
||||
> The `` in the URL is what pins the build to a specific commit: the
|
||||
> doc-test runner expands it to a concrete ref. Locally that is this checkout's
|
||||
> `HEAD` (see `run.sh`); in CI it is the commit being tested. With no pin it falls
|
||||
> back to the latest `master`. Developing against a local checkout? Replace the
|
||||
> GitHub reference with `.`, e.g. `nix build '.#lgx' -o lgx`.
|
||||
|
||||
### 1.1 Build lgx
|
||||
|
||||
```bash
|
||||
# From inside the clone this is simply: nix build '.#lgx' -o lgx
|
||||
nix build 'github:logos-co/logos-package/41cae9e4546899eb11912372588a6707ca43efe4#lgx' -o lgx
|
||||
```
|
||||
|
||||
The `-o lgx` flag names the result symlink, so the executable is at `./lgx/bin/lgx`.
|
||||
|
||||
### 1.2 Confirm it runs
|
||||
|
||||
```bash
|
||||
./lgx/bin/lgx --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Detect the platform variant
|
||||
|
||||
An `.lgx` package carries one or more **platform variants** — `linux-x86_64`,
|
||||
`darwin-arm64`, and so on — each holding the payload for that platform. Detect this
|
||||
machine's variant once (and the matching shared-library extension, `.so` on Linux /
|
||||
`.dylib` on macOS) and stash both in files so the packaging steps below can reuse
|
||||
them.
|
||||
|
||||
### 2.1 Write the variant and library extension to ./variant and ./ext
|
||||
|
||||
```bash
|
||||
case "$(uname -s) $(uname -m)" in
|
||||
"Linux x86_64") echo linux-x86_64 > variant; echo so > ext ;;
|
||||
"Linux aarch64") echo linux-arm64 > variant; echo so > ext ;;
|
||||
"Darwin arm64") echo darwin-arm64 > variant; echo dylib > ext ;;
|
||||
"Darwin x86_64") echo darwin-x86_64 > variant; echo dylib > ext ;;
|
||||
*) echo "unsupported platform: $(uname -sm)" >&2; exit 1 ;;
|
||||
esac
|
||||
echo "variant: $(cat variant) ext: $(cat ext)"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create a package
|
||||
|
||||
`lgx create <name>` writes an empty skeleton package — a `manifest.json` with
|
||||
sensible defaults plus an empty `variants/` directory — to `<name>.lgx`.
|
||||
|
||||
### 3.1 Create greeter.lgx
|
||||
|
||||
```bash
|
||||
./lgx/bin/lgx create greeter
|
||||
```
|
||||
|
||||
### 3.2 Read the fresh manifest
|
||||
|
||||
`lgx manifest` prints the embedded `manifest.json` in human-readable form. A
|
||||
freshly created package starts at version `0.0.1`, manifest schema `0.3.0`, with
|
||||
no type, no variants, and no signature yet.
|
||||
|
||||
```bash
|
||||
lgx manifest greeter.lgx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Add a platform variant
|
||||
|
||||
`lgx add` drops a payload into the package under `variants/<variant>/` and records
|
||||
the entry point in the manifest's `main` map. We add a stub shared library for this
|
||||
machine's variant. (In a real build the payload is the module's compiled `.so` /
|
||||
`.dylib`; `lgx` does not load it — it just packages it.)
|
||||
|
||||
### 4.1 Add this machine's variant
|
||||
|
||||
```bash
|
||||
# On Linux x86_64 this expands to:
|
||||
echo 'stub library' > libgreeter.so
|
||||
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
|
||||
```
|
||||
|
||||
Confirm the archive layout — a `manifest.json` beside the `variants/` tree:
|
||||
|
||||
```bash
|
||||
tar -tzf greeter.lgx
|
||||
```
|
||||
|
||||
```
|
||||
manifest.json
|
||||
variants/
|
||||
variants/<platform>/
|
||||
variants/<platform>/libgreeter.so # .dylib on macOS
|
||||
```
|
||||
|
||||
### 4.2 See the variant in the manifest
|
||||
|
||||
The manifest now lists the variant and points its `main` entry at the file we
|
||||
added. A content hash (`Root hash`) is recomputed on every save.
|
||||
|
||||
```bash
|
||||
lgx manifest greeter.lgx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Verify the package
|
||||
|
||||
`lgx verify` validates a package against the LGX spec — it recomputes the content
|
||||
hashes (the Merkle tree) and checks them against the manifest, then reports the
|
||||
signature state. Our package is unsigned, so verification passes and notes it is
|
||||
unsigned. The exit code is `0` on success, so `verify` is safe to gate CI on.
|
||||
|
||||
### 5.1 Verify greeter.lgx
|
||||
|
||||
```bash
|
||||
lgx verify greeter.lgx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Extract a variant
|
||||
|
||||
`lgx extract` unpacks a variant's payload back onto disk — the inverse of `add`.
|
||||
Files land under `<output>/<variant>/`.
|
||||
|
||||
### 6.1 Extract this machine's variant
|
||||
|
||||
```bash
|
||||
lgx extract greeter.lgx --variant linux-x86_64 --output ./extracted
|
||||
```
|
||||
|
||||
The payload is now on disk under `./extracted/<variant>/`:
|
||||
|
||||
```bash
|
||||
ls ./extracted/<variant>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Merge per-platform packages
|
||||
|
||||
Real releases ship one `.lgx` covering every platform. A common pattern is to build
|
||||
a **single-variant** package on each platform's CI runner, then `lgx merge` them
|
||||
into one multi-variant archive. The inputs must share identical metadata (only the
|
||||
variant-specific `main` entries differ). We simulate it here by hand-building a
|
||||
Linux and a macOS package for the same module and merging them.
|
||||
|
||||
### 7.1 Build two single-variant packages
|
||||
|
||||
```bash
|
||||
# A Linux-only package...
|
||||
./lgx/bin/lgx create widget && mv widget.lgx widget-linux.lgx
|
||||
echo 'linux stub' > libwidget.so
|
||||
./lgx/bin/lgx add widget-linux.lgx --variant linux-x86_64 --files libwidget.so
|
||||
|
||||
# ...and a macOS-only package for the same module.
|
||||
./lgx/bin/lgx create widget && mv widget.lgx widget-darwin.lgx
|
||||
echo 'darwin stub' > libwidget.dylib
|
||||
./lgx/bin/lgx add widget-darwin.lgx --variant darwin-arm64 --files libwidget.dylib
|
||||
|
||||
```
|
||||
|
||||
### 7.2 Merge them into one .lgx
|
||||
|
||||
`lgx merge` writes a single package carrying both variants. The variants are
|
||||
listed alphabetically in the summary.
|
||||
|
||||
```bash
|
||||
lgx merge widget-linux.lgx widget-darwin.lgx -o widget.lgx
|
||||
```
|
||||
|
||||
### 7.3 Confirm both variants are present
|
||||
|
||||
The merged manifest carries both platforms, and the package verifies:
|
||||
|
||||
```bash
|
||||
lgx manifest widget.lgx
|
||||
```
|
||||
|
||||
```bash
|
||||
lgx verify widget.lgx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recap
|
||||
|
||||
You built `lgx` from this commit and ran a full packaging lifecycle — creating a
|
||||
package, adding a platform variant, verifying it, extracting it, and merging
|
||||
per-platform packages into one multi-variant `.lgx`:
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `create <name>` | Write an empty skeleton `<name>.lgx` |
|
||||
| `add <pkg> -v <variant> -f <path>` | Add a payload for a variant (records `main`) |
|
||||
| `manifest <pkg> [--json]` | Print the embedded `manifest.json` (`--json` for raw bytes) |
|
||||
| `verify <pkg>` | Validate structure + content hashes, report signature state |
|
||||
| `extract <pkg> -v <variant> -o <dir>` | Unpack a variant's payload to disk |
|
||||
| `merge <pkg…> -o <out>` | Combine single-variant packages into one multi-variant `.lgx` |
|
||||
|
||||
Signing, signature inspection, and trusted-key management are covered in the
|
||||
companion [signing doc-test](lgx-signing.md).
|
||||
@@ -0,0 +1,199 @@
|
||||
# Signing and Trusting an lgx Package
|
||||
|
||||
[`lgx`](https://github.com/logos-co/logos-package) packages can be **signed** so
|
||||
consumers can verify who produced them. Signing uses an **Ed25519** keypair whose public
|
||||
half is published as a [`did:jwk:`](https://github.com/quartzjer/did-jwk/blob/main/spec.md)
|
||||
decentralized identifier; `lgx sign` writes a `manifest.sig` next to the manifest, and
|
||||
`lgx verify` checks it. A **keyring** of trusted DIDs lets `verify` go one step further
|
||||
and report whether the signer is one you trust.
|
||||
|
||||
This doc-test builds **this** `lgx` commit and runs the full signing-and-trust lifecycle:
|
||||
|
||||
1. Build `lgx` and create a small package to sign.
|
||||
2. `keygen` an Ed25519 keypair (printing its `did:jwk:` DID).
|
||||
3. `sign` the package and inspect the signature with `manifest` and `signature`.
|
||||
4. `verify` it — first as an *untrusted* signer, then after adding the DID to a keyring.
|
||||
|
||||
Every key directory is passed explicitly (`--output-dir`, `--keys-dir`, `--keyring-dir`)
|
||||
so the whole flow is self-contained and never touches your real `~/.config/logos`.
|
||||
|
||||
**What you'll build:** A signed `greeter.lgx`, an Ed25519 signing key, and a trusted-key keyring that makes `lgx verify` recognise the signer.
|
||||
|
||||
**What you'll learn:**
|
||||
|
||||
- How to generate an Ed25519 signing keypair and its `did:jwk:` DID with `lgx keygen`
|
||||
- How to sign a package and read back its signature with `lgx sign` / `lgx signature`
|
||||
- How `lgx verify` reports signature validity and distinguishes trusted from untrusted signers
|
||||
- How to manage trusted DIDs with `lgx keyring add` / `list`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
||||
|
||||
- A Linux or macOS machine.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Build lgx and create a package to sign
|
||||
|
||||
Build the `lgx` CLI from this commit, then create a one-variant package — the thing
|
||||
we will sign. (The [packaging doc-test](lgx-cli.md) covers `create` / `add` in
|
||||
detail; here they are just setup.)
|
||||
|
||||
> The `` placeholder pins the build to the commit under test (this
|
||||
> checkout's `HEAD` locally, the PR/push commit in CI). Against a local checkout use
|
||||
> `nix build '.#lgx' -o lgx`.
|
||||
|
||||
### 1.1 Build lgx
|
||||
|
||||
```bash
|
||||
# From inside the clone this is simply: nix build '.#lgx' -o lgx
|
||||
nix build 'github:logos-co/logos-package/41cae9e4546899eb11912372588a6707ca43efe4#lgx' -o lgx
|
||||
```
|
||||
|
||||
### 1.2 Create and populate greeter.lgx
|
||||
|
||||
Detect this machine's variant (and shared-library extension), create the
|
||||
package, and add a stub payload so there is content to sign.
|
||||
|
||||
```bash
|
||||
lgx create greeter
|
||||
echo 'stub library' > libgreeter.so # .dylib on macOS
|
||||
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Generate a signing key
|
||||
|
||||
`lgx keygen` creates an Ed25519 keypair and writes three files — the secret key
|
||||
(`.jwk`), the public key (`.pub`), and the DID string (`.did`). It prints the
|
||||
`did:jwk:` DID to stdout. We point `--output-dir` at a local `./keys` directory and
|
||||
stash the DID in a file so later steps can refer to it.
|
||||
|
||||
### 2.1 Run keygen into ./keys
|
||||
|
||||
`keygen` prints the DID to stdout and also writes it to `./keys/demo-key.did`. We
|
||||
copy that file to `./did.txt` so later steps can pass the exact DID without
|
||||
re-deriving it.
|
||||
|
||||
```bash
|
||||
lgx keygen --name demo-key --output-dir ./keys
|
||||
# → did:jwk:eyJjcnYiOiJFZDI1NTE5Ii... (also saved to ./keys/demo-key.did)
|
||||
```
|
||||
|
||||
The keypair now lives under `./keys/` as `demo-key.{jwk,pub,did}`. The `.jwk`
|
||||
file is the **secret** key — keep it safe; only the DID is meant to be shared.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Sign the package
|
||||
|
||||
`lgx sign` validates the package, then writes a `manifest.sig` containing the
|
||||
signer's DID, an Ed25519 signature over the manifest bytes, and optional
|
||||
self-asserted signer metadata (`--name` / `--url`). We pass `--keys-dir ./keys` so it
|
||||
finds the key we just generated.
|
||||
|
||||
### 3.1 Sign greeter.lgx
|
||||
|
||||
```bash
|
||||
lgx sign greeter.lgx --key demo-key --keys-dir ./keys \
|
||||
--name "Logos Demo" --url "https://logos.co"
|
||||
```
|
||||
|
||||
### 3.2 See the signature in the manifest
|
||||
|
||||
`lgx manifest` now reports the package as signed, along with the signer's DID and
|
||||
self-asserted name/URL.
|
||||
|
||||
```bash
|
||||
lgx manifest greeter.lgx
|
||||
```
|
||||
|
||||
### 3.3 Read the raw signature
|
||||
|
||||
`lgx signature` prints the raw `manifest.sig` JSON — byte-identical to the file
|
||||
inside the `.lgx`. It carries the algorithm, the signer DID, the base64
|
||||
signature, and the signer metadata. Unsigned packages print nothing and exit 0,
|
||||
so tooling tells "unsigned" from "error" by the exit status, not the output.
|
||||
|
||||
```bash
|
||||
lgx signature greeter.lgx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Verify an untrusted signer
|
||||
|
||||
`lgx verify` always checks structure and (for signed packages) the signature itself.
|
||||
Trust is separate: with no matching entry in the keyring, the signature is reported
|
||||
**valid** but the signer is flagged as **not** trusted. We point `--keyring-dir` at an
|
||||
empty `./trusted` directory to make that explicit.
|
||||
|
||||
### 4.1 Verify against an empty keyring
|
||||
|
||||
```bash
|
||||
mkdir -p trusted
|
||||
lgx verify greeter.lgx --keyring-dir ./trusted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Trust the signer
|
||||
|
||||
`lgx keyring add` records a DID as trusted under a short name. We add the DID we
|
||||
stashed earlier (still in `did.txt`) to the `./trusted` keyring, then list it.
|
||||
|
||||
### 5.1 Add the DID to the keyring
|
||||
|
||||
```bash
|
||||
lgx keyring add logos-demo "$DID" \
|
||||
--display-name "Logos Demo" --url "https://logos.co" --dir ./trusted
|
||||
```
|
||||
|
||||
### 5.2 List trusted keys
|
||||
|
||||
```bash
|
||||
lgx keyring list --dir ./trusted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Verify a trusted signer
|
||||
|
||||
Re-run `verify` against the populated keyring. Now that the signer's DID is trusted,
|
||||
`verify` adds a final line naming the trusted key — the same valid signature, now
|
||||
recognised.
|
||||
|
||||
### 6.1 Verify against the populated keyring
|
||||
|
||||
```bash
|
||||
lgx verify greeter.lgx --keyring-dir ./trusted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recap
|
||||
|
||||
You built `lgx` from this commit and ran the full signing-and-trust lifecycle —
|
||||
generating a key, signing a package, inspecting the signature, and verifying it both
|
||||
as an untrusted and a trusted signer:
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `keygen --name <n> --output-dir <dir>` | Generate an Ed25519 keypair; print its `did:jwk:` DID |
|
||||
| `sign <pkg> --key <n> --keys-dir <dir> [--name …] [--url …]` | Write `manifest.sig` with the signer's DID |
|
||||
| `signature <pkg>` | Print the raw `manifest.sig` JSON (unsigned → empty + exit 0) |
|
||||
| `verify <pkg> [--keyring-dir <dir>]` | Validate structure + signature; report trust |
|
||||
| `keyring add <name> <did> [--dir <dir>]` | Trust a DID under a short name |
|
||||
| `keyring list [--dir <dir>]` | List trusted DIDs |
|
||||
|
||||
Creating, adding variants, extracting, and merging packages are covered in the
|
||||
companion [packaging doc-test](lgx-cli.md).
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Execute every lgx doc-test end-to-end and regenerate its Markdown.
|
||||
#
|
||||
# The runner is the shared `doctest` CLI
|
||||
# (https://github.com/logos-co/logos-doctest), invoked directly via its flake.
|
||||
# For each *.test.yaml in this directory, `doctest run` executes every command in
|
||||
# a temp directory (building lgx, creating/signing/verifying .lgx packages) and
|
||||
# asserts on the output; `doctest generate` renders the same spec to Markdown
|
||||
# under outputs/ (the *.test.yaml spec is the source of truth).
|
||||
#
|
||||
# To run against a local logos-doctest checkout instead of the published flake,
|
||||
# set DOCTEST, e.g.: DOCTEST="nix run path:../../logos-doctest --" ./run.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# Run from this doctests/ directory regardless of where the script is invoked from.
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# The doctest CLI. Override by exporting DOCTEST (space-separated command).
|
||||
read -r -a DOCTEST <<< "${DOCTEST:-nix run github:logos-co/logos-doctest --}"
|
||||
|
||||
# Build the doc-tests against THIS repo's current commit rather than the latest
|
||||
# published flake. Each spec's `github:logos-co/logos-package{release}` URL is
|
||||
# pinned to $COMMIT via --release-for, so the run exercises exactly what's checked
|
||||
# out here. Override by exporting COMMIT (e.g. a tag), or set COMMIT="" to fall
|
||||
# back to latest.
|
||||
#
|
||||
# Note: nix fetches the commit from the GitHub remote, so $COMMIT must be pushed
|
||||
# to logos-co/logos-package. A local-only / uncommitted HEAD won't resolve;
|
||||
# export COMMIT="" (or push first) in that case.
|
||||
COMMIT="${COMMIT-$(git rev-parse HEAD)}"
|
||||
RELEASE_FOR=()
|
||||
if [ -n "${COMMIT}" ]; then
|
||||
RELEASE_FOR=(--release-for "logos-package=${COMMIT}")
|
||||
echo "==> Pinning logos-package to ${COMMIT}"
|
||||
else
|
||||
echo "==> COMMIT empty; building logos-package from latest"
|
||||
fi
|
||||
|
||||
mkdir -p outputs
|
||||
|
||||
for spec in *.test.yaml; do
|
||||
name="$(basename "${spec%.test.yaml}")"
|
||||
echo "==> Running ${spec}"
|
||||
# ${RELEASE_FOR[@]+...} guards the expansion so an empty array doesn't trip
|
||||
# `set -u` on older bash (e.g. macOS's stock 3.2).
|
||||
"${DOCTEST[@]}" run "${spec}" --verbose --continue-on-fail \
|
||||
${RELEASE_FOR[@]+"${RELEASE_FOR[@]}"}
|
||||
|
||||
echo "==> Generating outputs/${name}.md"
|
||||
"${DOCTEST[@]}" generate "${spec}" \
|
||||
${RELEASE_FOR[@]+"${RELEASE_FOR[@]}"} \
|
||||
-o "outputs/${name}.md"
|
||||
done
|
||||
|
||||
echo "==> Done. Rendered docs are in doctests/outputs/"
|
||||
Reference in New Issue
Block a user