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
2 changes: 1 addition & 1 deletion DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# picasso (formerly nix-config), for the LinkedIn Projects section

Self-contained Nix flakes ("labs") that pin scientific, publishing and agentic toolchains for other repositories, with deterministic builds. Ten labs: WAVEWATCH III (gfortran, OpenMPI, NetCDF), pandoc + TeX Live publishing with its own GitHub Action, Markdown to PDF for Portuguese in one Rust + Typst binary (25.2 MiB static, against 729 MiB for pandoc + XeLaTeX), Markdown papers for arXiv, a Gleam toolchain for static sites, a lint set, a sandbox for coding agents, CUDA and PyTorch host setup, a presenter's desk, and a code graph of any repository. The code is only Nix and Rust. Any repo can borrow a lab as a flake input, a submodule or an Action, and CI builds each one on every push. It is the toolchain layer under WW3 GPU Lab, marola and ptbr-writer. Inspired by gvolpe/nix-config.
Self-contained Nix flakes ("labs") that pin scientific, publishing and agentic toolchains for other repositories, with deterministic builds. Eleven labs: WAVEWATCH III (gfortran, OpenMPI, NetCDF), pandoc + TeX Live publishing with its own GitHub Action, Markdown to PDF for Portuguese in one Rust + Typst binary (25.2 MiB static, against 729 MiB for pandoc + XeLaTeX), Markdown papers for arXiv, a Gleam toolchain for static sites, a lint set, a Rust toolchain for a weather-alert agent, a sandbox for coding agents, CUDA and PyTorch host setup, a presenter's desk, and a code graph of any repository. The code is only Nix and Rust. Any repo can borrow a lab as a flake input, a submodule or an Action, and CI builds each one on every push. It is the toolchain layer under WW3 GPU Lab, marola and ptbr-writer. Inspired by gvolpe/nix-config.

## One line (the GitHub "About" box)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Each lab README has the recipes, what the lab pins (`What it pins`) and how to r
| [`ptbr-pdf`](labs/ptbr-pdf) | Markdown to PDF for Portuguese: one Rust binary, `to-pdf`, Typst as a library, six fonts inside | `nix run github:h0ffmann/picasso?dir=labs/ptbr-pdf -- texto.md` |
| [`ptbr-tools`](labs/ptbr-tools) | The ptbr-writer plugin's runtime scripts (hook, reading time, router) as one static Rust binary, 0.54 MiB, std only | `nix build github:h0ffmann/picasso?dir=labs/ptbr-tools#static` |
| [`lint`](labs/lint) | hadolint, actionlint, shellcheck, ruff, pyflakes, cloc, coverage.py, pdoc as one list | `lib.<system>.tools` in your `mkShell` |
| [`rust-agent`](labs/rust-agent) | rustc, cargo, clippy, rust-analyzer, nextest, deny, audit, machete, jq, xh for a Rust CLI that reads weather alerts and calls an LLM | `lib.<system>.tools` in your `mkShell` |
| [`paper`](labs/paper) | Markdown papers to PDF and arXiv source: pandoc-crossref, XeLaTeX, pdfLaTeX | `just shell`, `just smoke` |
| [`gleam-website`](labs/gleam-website) | Gleam (JavaScript target), node, esbuild, just: the toolchain of this repo's site | `just shell`, `just smoke` |
| [`agentic`](labs/agentic) | ai-jail, OpenCode, Open Code Review, gh; `jail-run`, `gh-token`, `clip`, `clip-relay` | `just jco`, `just test` |
Expand Down
3 changes: 3 additions & 0 deletions labs/rust-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/result
.direnv/
/smoke/target/
59 changes: 59 additions & 0 deletions labs/rust-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# rust-agent

The tools to build a Rust CLI that reads official Brazilian weather alerts (INMET, CEMADEN,
Defesa Civil: JSON, XML, CAP and RSS feeds), calls an LLM over Anthropic's HTTP API, and writes
static reports. They come from one locked nixpkgs. Anthropic has no official Rust SDK, so the
agent uses plain HTTPS and JSON. This lab ships the toolchain only. The agent code is in the
consumer.

```
just shell # the tools on PATH, in your current directory (RUST_SRC_PATH is set)
just versions # every tool's --version, built in the Nix sandbox (what CI checks)
just smoke # build, test, clippy and rustfmt the smoke crate in the sandbox (what CI builds)
just test # cargo test in the smoke crate, in the dev shell
```

## Reusing it from another repository

```nix
{
inputs.rust-agent = { url = "github:h0ffmann/picasso?dir=labs/rust-agent"; inputs.nixpkgs.follows = "nixpkgs"; };
outputs = { nixpkgs, rust-agent, ... }: {
devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell {
packages = [ /* the project's own tools */ ] ++ rust-agent.lib.x86_64-linux.tools;
RUST_SRC_PATH = "${nixpkgs.legacyPackages.x86_64-linux.rustPlatform.rustLibSrc}";
};
};
}
```

`follows` keeps one nixpkgs closure in the consumer's shell. The price is that this lab's CI
uses its own lock and the consumer uses its own, so a tool can differ by a patch version.
`nix flake update rust-agent` in the consumer picks up a new revision of this lab.

`packages.<system>.<tool>` re-exports each tool for `nix run`. `checks.<system>.versions` fails
the flake check if a nixpkgs bump breaks a tool. `checks.<system>.smoke` builds and tests a
small std-only crate (`smoke/`) with the pinned toolchain, and runs `clippy -D warnings` and
`rustfmt --check` on it.

No consumer yet. The first one will be [marola-site](https://github.com/marola-dev/marola-site)
(issue 172).

## TLS

Prefer `rustls` in the agent's crates. A static musl build then needs neither `openssl` nor
`pkg-config`. The lab pins both only for crates that need native TLS.

## What it pins

| Type | Program |
| :--- | :-----: |
| Compiler | [rustc and cargo](https://www.rust-lang.org/) |
| Lint and format | [clippy](https://github.com/rust-lang/rust-clippy), [rustfmt](https://github.com/rust-lang/rustfmt) |
| Editor | [rust-analyzer](https://rust-analyzer.github.io/) |
| Tests | [cargo-nextest](https://nexte.st/) |
| Supply chain | [cargo-deny](https://github.com/EmbarkStudios/cargo-deny) (licences, bans), [cargo-audit](https://github.com/rustsec/rustsec) (advisories) |
| Unused crates | [cargo-machete](https://github.com/bnjbvr/cargo-machete) (works on stable; cargo-udeps needs nightly) |
| Native TLS | [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/), [openssl](https://www.openssl.org/) |
| Feeds and API | [jq](https://jqlang.github.io/jq/), [xh](https://github.com/ducaale/xh) |
| Reuse | `lib.<system>.tools` (the list), `packages.<system>.<tool>`, `checks.<system>.{lab-json,versions,smoke}` |
27 changes: 27 additions & 0 deletions labs/rust-agent/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 123 additions & 0 deletions labs/rust-agent/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
description = "rust-agent — the Rust toolchain for a CLI that reads Brazilian weather alerts and calls an LLM over HTTPS: rustc, cargo, clippy, rustfmt, rust-analyzer, nextest, deny, audit, machete";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};

outputs = { self, nixpkgs }:
let
inherit (nixpkgs) lib;
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAll = f: lib.genAttrs systems (s: f nixpkgs.legacyPackages.${s});

# The one place the tools are named. A consumer appends this to its own mkShell packages.
toolsFor = pkgs: with pkgs; [
rustc
cargo
clippy
rustfmt
rust-analyzer
cargo-nextest
cargo-deny
cargo-audit
cargo-machete # reads Cargo.toml and the sources; cargo-udeps needs nightly
pkg-config
openssl # only for crates with native TLS; prefer rustls (see README)
jq
xh
];

# `nix flake check` builds this: every tool answers --version in the sandbox, so a nixpkgs
# bump that drops or breaks one fails here, not in a consumer's CI.
versionsFor = pkgs: pkgs.runCommand "rust-agent-versions" { nativeBuildInputs = toolsFor pkgs; } ''
{
echo "nixpkgs ${nixpkgs.rev or "dirty"}"
echo "rustc $(rustc --version)"
echo "cargo $(cargo --version)"
echo "clippy $(cargo-clippy --version)"
echo "rustfmt $(rustfmt --version)"
echo "rust-analyzer $(rust-analyzer --version)"
echo "nextest $(cargo-nextest nextest --version | head -1)"
echo "deny $(cargo-deny --version)"
echo "audit $(cargo-audit --version)"
echo "machete $(cargo-machete --version)"
echo "pkg-config $(pkg-config --version)"
echo "openssl $(openssl version)"
echo "jq $(jq --version)"
echo "xh $(xh --version | head -1)"
} | tee versions.txt
mkdir -p "$out" && cp versions.txt "$out"/
'';

# No dependencies, so no vendoring: the lock file has only the crate itself. Tests run in
# the build (checkPhase).
smokeFor = pkgs: pkgs.rustPlatform.buildRustPackage {
pname = "rust-agent-smoke";
version = "0.1.0";
src = lib.fileset.toSource {
root = ./smoke;
fileset = lib.fileset.unions [ ./smoke/Cargo.toml ./smoke/Cargo.lock ./smoke/src ];
};
cargoLock.lockFile = ./smoke/Cargo.lock;
meta.mainProgram = "rust-agent-smoke";
};

# Same sources, `cargo clippy -D warnings` as the build.
clippyFor = pkgs: (smokeFor pkgs).overrideAttrs (old: {
pname = "rust-agent-smoke-clippy";
nativeBuildInputs = old.nativeBuildInputs ++ [ pkgs.clippy ];
buildPhase = "cargo clippy --release --all-targets --offline -- -D warnings";
doCheck = false;
installPhase = "touch $out";
});

fmtFor = pkgs: pkgs.runCommand "rust-agent-smoke-fmt" { nativeBuildInputs = [ pkgs.rustfmt ]; } ''
rustfmt --check --edition 2021 ${./smoke/src/main.rs}
touch $out
'';

# lab.json, the card the profile README renders: its shape is asserted at evaluation, and the
# build writes each headline attribute's version from this lab's own nixpkgs. Copied in every
# lab, since a lab references nothing outside itself.
labCardFor = pkgs:
let
card = lib.importJSON ./lab.json;
version = attr: (lib.getAttrFromPath (lib.splitString "." attr) pkgs).version;
in
assert lib.assertMsg (lib.isString card.summary && card.summary != "" && lib.stringLength card.summary <= 100)
"lab.json: summary must be one non-empty line of at most 100 characters";
assert lib.assertMsg (lib.isList card.headline && card.headline != [ ] && lib.all lib.isString card.headline)
"lab.json: headline must be a non-empty list of nixpkgs attribute paths";
pkgs.writeText "lab-json" (lib.concatMapStrings (a: "${a} ${version a}\n") card.headline);
in
{
lib = forAll (pkgs: {
tools = toolsFor pkgs;
});

packages = forAll (pkgs: {
versions = versionsFor pkgs;
inherit (pkgs) rustc cargo clippy rustfmt rust-analyzer cargo-nextest cargo-deny cargo-audit cargo-machete pkg-config openssl jq xh;
});

devShells = forAll (pkgs: {
default = pkgs.mkShell {
name = "rust-agent";
packages = toolsFor pkgs;
RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
shellHook = ''echo "rust-agent: $(rustc --version) | $(cargo --version)"'';
};
});

checks = forAll (pkgs: {
lab-json = labCardFor pkgs;
versions = self.packages.${pkgs.stdenv.hostPlatform.system}.versions;
smoke = pkgs.linkFarm "rust-agent-smoke-checks" [
{ name = "build"; path = smokeFor pkgs; }
{ name = "clippy"; path = clippyFor pkgs; }
{ name = "fmt"; path = fmtFor pkgs; }
];
});
};
}
22 changes: 22 additions & 0 deletions labs/rust-agent/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# rust-agent — the Rust toolchain for a climate-agent CLI. `just` lists recipes.
set shell := ["bash", "-euo", "pipefail", "-c"]
here := justfile_directory()

default:
@just --list

# Enter the shell: the Rust toolchain and the feed tools on PATH, in your current directory.
shell:
nix develop "{{here}}"

# Print the exact tool versions this lab pins (builds checks.versions in the sandbox).
versions:
nix build "{{here}}#versions" --print-build-logs --out-link "{{here}}/result" && cat "{{here}}/result/versions.txt"

# Build and test the std-only smoke crate in the sandbox, with clippy and rustfmt (what CI builds).
smoke:
nix build "{{here}}#checks.$(nix eval --impure --raw --expr builtins.currentSystem).smoke" --print-build-logs --no-link

# cargo test in the smoke crate, inside the dev shell.
test:
nix develop "{{here}}" --command cargo test --manifest-path "{{here}}/smoke/Cargo.toml"
4 changes: 4 additions & 0 deletions labs/rust-agent/lab.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"summary": "rustc, cargo, clippy, rust-analyzer, nextest, deny, audit for a Rust agent, one list",
"headline": ["rustc", "cargo", "rust-analyzer", "cargo-nextest"]
}
7 changes: 7 additions & 0 deletions labs/rust-agent/smoke/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions labs/rust-agent/smoke/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "rust-agent-smoke"
version = "0.1.0"
edition = "2021"

[dependencies]
17 changes: 17 additions & 0 deletions labs/rust-agent/smoke/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
fn main() {
println!("{}", greeting("climate-agent"));
}

fn greeting(name: &str) -> String {
format!("hello, {name}")
}

#[cfg(test)]
mod tests {
use super::greeting;

#[test]
fn greets_by_name() {
assert_eq!(greeting("inmet"), "hello, inmet");
}
}
Loading