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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to Raven are documented in this file.

## [2.20.0] - 2026-07-03

### Added

- `rvpm dist` packages the built application into distributable artifacts: `tar`, `zip`, `deb`, `rpm`, `msi` (WiX 3), and `inno` (Inno Setup), configured by an optional `[dist]` manifest section (metadata, extra assets, Linux dependencies, installer icon and upgrade GUID) with `--target` and `--out-dir` overrides. Control characters are rejected in `[dist]` metadata and the package version so generated packaging files cannot be injected. See `docs/v2/specs/rvpm-dist.md`. (#857)

## [2.19.17] - 2026-07-03

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
members = [".", "raven-runtime"]

[workspace.package]
version = "2.19.17"
version = "2.20.0"
edition = "2021"
authors = ["martian56"]
license = "MIT"
Expand Down
87 changes: 87 additions & 0 deletions docs/v2/specs/rvpm-dist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# rvpm dist

`rvpm dist` builds the application in the current directory and packages it
into distributable artifacts. A Raven binary is a single static file, so
packaging is one staged file tree plus each format's own metadata, which
rvpm generates from the manifest.

## Usage

```
rvpm dist [--target <t1,t2>] [--out-dir <dir>]
```

Targets: `tar`, `zip`, `deb`, `rpm`, `msi`, `inno`. `--target` overrides the
manifest's `[dist].targets`; without either, the host default applies
(`zip` on Windows, `tar` elsewhere). Libraries have no binary and are
rejected.

Artifacts land in `[dist].out_dir` (default `target/dist`), with a `work/`
scratch directory beside them holding the staging trees and generated
packaging files, which is useful when debugging a format.

## The [dist] manifest section

Every field is optional; an absent section behaves like an empty one.

```toml
[dist]
targets = ["deb", "zip"] # what plain `rvpm dist` produces
out_dir = "target/dist"
display_name = "Rook" # installer titles; default: package name
description = "A coding agent" # default: "<name> <version>"
license = "MIT" # rpm License, installers
homepage = "https://example.com" # deb Homepage, rpm URL, installers
maintainer = "Ada <ada@x.com>" # deb Maintainer; default: first author
vendor = "Acme" # rpm Vendor, msi Manufacturer; default: maintainer

[[dist.assets]] # extra files installed with the binary
source = "README.md" # read relative to the package root
dest = "share/doc/rook/README.md" # forward-slash path under the install prefix

[dist.linux]
depends = ["libc6 (>= 2.31)"] # deb Depends and rpm Requires, verbatim
section = "utils" # deb
priority = "optional" # deb

[dist.windows]
icon = "assets/rook.ico" # Inno Setup icon
upgrade_code = "9f0c86a1-2b3c-4d5e-8f90-112233445566" # msi upgrade GUID
```

Asset `source` and `dest` must be relative forward-slash paths with no `..`
components; the same containment rule `[ffi].sources` follows. The install
prefix per format: `/usr/` for deb and rpm (the binary goes to
`/usr/bin/<name>`), the archive root for tar and zip, and the application
folder for msi and inno.

## Formats and their tools

rvpm generates each format's packaging text and shells out to the format's
own tool, the same approach dependency fetching takes with curl and tar. A
missing tool fails with an install hint.

| Target | Tool | Artifact |
|---|---|---|
| `tar` | tar | `<name>-<version>-<arch>.tar.gz`, top-level `<name>-<version>-<arch>/` directory |
| `zip` | bsdtar or zip | `<name>-<version>-<arch>.zip`, flat |
| `deb` | dpkg-deb | `<name>_<version>_<deb-arch>.deb` |
| `rpm` | rpmbuild | rpmbuild's own `<name>-<version>-1.<arch>.rpm` naming |
| `msi` | WiX 3 (candle, light) | `<name>-<version>-<arch>.msi` |
| `inno` | Inno Setup ISCC | `<name>-<version>-setup.exe` |

Version strings are normalized per format: a leading `v` is dropped
everywhere, rpm turns a `-` pre-release separator into `~`, and the msi
ProductVersion keeps only the numeric `X.Y.Z` prefix.

The msi upgrade GUID is what lets a newer installer replace an older
install. When `[dist.windows].upgrade_code` is not set, rvpm derives a
stable GUID from the package name and prints it with a note; pin it in the
manifest so it never changes by accident.

## What is out of scope

Packages are not signed (deb signing, rpm signing, Authenticode are all
post-processing on the produced artifacts). Cross-format scripting hooks
(postinst and friends) and desktop shortcuts are not modeled yet. The
installers do not modify PATH.
69 changes: 69 additions & 0 deletions src/bin/rvpm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ fn dispatch() -> ExitCode {
Some("install") => run(cmd_install(&args[1..])),
Some("update") => run(cmd_update(&args[1..])),
Some("build") => run(cmd_build(&args[1..])),
Some("dist") => run(cmd_dist(&args[1..])),
Some("run") => cmd_run(&args[1..]),
Some("test") => cmd_test(&args[1..]),
Some("doc") => run(cmd_doc(&args[1..])),
Expand Down Expand Up @@ -572,6 +573,58 @@ fn cmd_build(args: &[String]) -> Result<Vec<String>, String> {
Ok(report.outcome_lines)
}

/// Build the application, then package it into distributable artifacts
/// (archives, deb, rpm, msi, Inno Setup) per the manifest's `[dist]`
/// section and any command-line overrides.
fn cmd_dist(args: &[String]) -> Result<Vec<String>, String> {
if args.iter().any(|a| a == "--help" || a == "-h") {
return Ok(vec![dist_usage()]);
}
let mut opts = ops::dist::DistOptions::default();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--target" => {
i += 1;
let value = args.get(i).ok_or_else(|| {
"--target needs a value, for example --target deb,zip".to_string()
})?;
for t in value.split(',') {
let t = t.trim();
if t.is_empty() {
continue;
}
if !raven::manifest::DIST_TARGETS.contains(&t) {
return Err(format!(
"'{}' is not a dist target; use any of {}",
t,
raven::manifest::DIST_TARGETS.join(", ")
));
}
opts.targets.push(t.to_string());
}
}
"--out-dir" => {
i += 1;
let value = args
.get(i)
.ok_or_else(|| "--out-dir needs a directory".to_string())?;
opts.out_dir = Some(value.clone());
Comment thread
martian56 marked this conversation as resolved.
}
other => {
return Err(format!(
"unknown argument '{}' for `rvpm dist`; run `rvpm dist --help`",
other
));
}
}
i += 1;
}
let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
let report = with_fetch_progress(|| ops::dist::dist(&cwd, opts)).map_err(|e| e.to_string())?;
Ok(report.outcome_lines)
}

/// Build the package then run the produced binary, forwarding any args
/// after `run` to the program and exiting with its code.
fn cmd_run(args: &[String]) -> ExitCode {
Expand Down Expand Up @@ -856,6 +909,21 @@ fn build_usage() -> String {
"Usage: rvpm build".to_string()
}

fn dist_usage() -> String {
[
"Usage: rvpm dist [--target <t1,t2>] [--out-dir <dir>]",
"",
"Build the application, then package it into distributable artifacts",
"under target/dist. Targets: tar, zip, deb, rpm, msi, inno.",
"",
"Without --target, the manifest's [dist].targets applies; without a",
"[dist] section, the host default does (zip on Windows, tar elsewhere).",
"Metadata, extra files, and installer settings live in the optional",
"[dist] section of rv.toml; see docs/v2/specs/rvpm-dist.md.",
]
.join("\n")
}

fn run_usage() -> String {
"Usage: rvpm run [program arguments]".to_string()
}
Expand Down Expand Up @@ -893,6 +961,7 @@ fn print_usage() {
println!(" install Resolve rv.toml against rv.lock and fill the cache");
println!(" update [pkg] Re-resolve rv.toml and rewrite rv.lock for one package or all");
println!(" build Compile src/main.rv to a binary, or type-check a lib.rv library");
println!(" dist Package the built application (tar, zip, deb, rpm, msi, inno)");
println!(" run [args] Build the application then run it, forwarding args");
println!(" test Run fun test_*() tests in *_test.rv files");
println!(" doc Generate Markdown API docs into target/doc");
Expand Down
Loading
Loading