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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ jobs:
run: |
mkdir -p /tmp/debmagic
current="$(dpkg-parsechangelog -SSource)-$(dpkg-parsechangelog -SVersion)"
find /tmp/debmagic -mindepth 1 -maxdepth 1 -type d ! -name "$current" -exec rm -rf {} +
find /tmp/debmagic -mindepth 1 -maxdepth 1 -type d \
! -name "$current" ! -name "${current}-test" \
-exec rm -rf {} +
- name: Resolve CI image tag
id: image
run: |
Expand All @@ -181,6 +183,11 @@ jobs:
--persistent \
--incremental \
--driver-docker-base-image="${{ steps.image.outputs.tag }}"
- name: Run Debmagic test on ourself
run: |
cargo run --locked -p debmagic -- test \
--driver=docker \
--driver-docker-base-image="${{ steps.image.outputs.tag }}"
- name: Push the CI image
if: github.ref == 'refs/heads/main' && env.image_built == 'true'
run: |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ To learn packaging with debmagic, follow **[the documentation!](https://debmagic
| - | - |
| `debmagic build binary` | Build a binary package in a container |
| `debmagic build source` | Create a source package for upload |
| `debmagic test` | Run package tests |
| `debmagic test` | Run Debian autopkgtest tests (`debian/tests/`) against a prior build |
| `debmagic check` | Lint the package |

> [!TIP]
Expand Down
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Source: debmagic
Testsuite: autopkgtest
Section: devel
Maintainer: Debmagic Maintainers <debmagic@sft.lol>
Uploaders:
Expand Down
2 changes: 2 additions & 0 deletions debian/tests/control
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Tests: smoke
Depends: @
5 changes: 5 additions & 0 deletions debian/tests/smoke
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -e

debmagic --version
debmagic test --help
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

usage/getting-started.md
usage/build.md
usage/test.md
usage/source.md
usage/config.md
usage/modules/index.md
Expand Down
6 changes: 4 additions & 2 deletions docs/usage/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ debmagic build binary --driver lxd \
| `--apt-mirror <url>` | [Mirror URL](#mirror-selection) |
| `--source-dir <dir>` | Directory containing the `debian/` package directory |
| `--output-dir <dir>` | Directory to put the resulting build artifacts |
| `--shell-on-failure` | On build failure, drop into an interactive shell in the build environment when stdout is a TTY |
Comment thread
mikonse marked this conversation as resolved.

[`debmagic shell`](#inspecting-a-failed-build) — attach an interactive shell to a build environment

Expand All @@ -49,8 +50,9 @@ There's no auto-detection; pick one and pass it explicitly every time (or config

## Inspecting a failed build

By default a failed build tears down the container, so nothing is left to inspect.
If a build might fail and you need to inspect it afterwards, pass `--persistent` up front, then once the run finishes:
On failure the build environment is torn down by default. Pass `--shell-on-failure` to drop into an interactive shell inside the build environment when stdout is a TTY (destroyed on shell exit unless `--persistent` was used).

To inspect after the run finishes, pass `--persistent` up front, then:

```shell
# if you're in the package still
Expand Down
82 changes: 82 additions & 0 deletions docs/usage/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Running package tests

Quick reference for running a package's declared Debian autopkgtest tests with `debmagic test`.

## TL;DR

- Entry point: `debmagic test` — runs tests from `debian/tests/control`; needs a prior `debmagic build`
- Requires a completed build in the same build root (or pass `--changes` to point at exported artifacts)

```shell
cd your-package
debmagic build binary --driver docker
debmagic test --driver docker
```

## What it does

`debmagic test` installs the binary packages from a prior build and runs the package's declared autopkgtest tests (`debian/tests/control`)
inside a **fresh, separate** driver-managed environment.
The test environment is never the build environment — even when `--persistent` reuses a container across runs,
the test tree is reset and the `.debs` are reinstalled each time.

The driver *is* the testbed: `autopkgtest` runs with the `null` backend inside the container (or on the host for the bare driver). No `autopkgtest-virt-*` backends are used.

## Available options

| Option | Description |
|---|---|
| `--driver <...>` | Test environment driver (defaults to the driver recorded in the prior build's `environment.json`) |
| `--persistent` | Retain the test environment after the run for reattach/debug |
| `--strict` | Treat skipped tests and "no tests declared" as failures (exit code 2) |
| `--changes <path>` | Path to a `.changes` file whose directory supplies the built `.debs` (for pipeline use) |
| `--distro <name>` | Override the target distro for the test environment (defaults to the prior build's distro from `environment.json`, not the changelog) |
| `--proposed` | Enable the `<release>-proposed` pocket in the test environment |
| `--apt-mirror <url>` | Mirror URL (same as [`debmagic build`](build.md)) |
| `--source-dir <dir>` | Directory containing the `debian/` package directory |
| `--allow-host-test` | Allow the bare driver, which runs autopkgtest as root on the host |
| `--shell-on-failure` | On test failure, drop into an interactive shell in the test environment when stdout is a TTY |

Driver-specific flags (`--driver-docker-base-image`, `--driver-lxd-*`) mirror `debmagic build`.

## Picking a driver

Use the same drivers as for builds. Pass `--driver` explicitly (or rely on the driver recorded in the prior build's `environment.json`):

| Driver | Isolation the Environment provides |
|---|---|
| `lxd` / `incus` | Container (`isolation-container`) |
| `docker` | Container (`isolation-container`) |
| `bare` | None — tests run as root on the host; requires `--allow-host-test` |

The driver *is* the testbed, so autopkgtest is told to run tests whose isolation restrictions the environment actually satisfies (`--ignore-restrictions`, only for those rungs). Tests that declare `Restrictions: isolation-container` therefore run on Docker/LXD/Incus instead of skipping. `isolation-machine` is not provided by any current driver (none is a VM); those tests still skip. Bare provides nothing, even with `--allow-host-test`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lxd can be a vm - for convenience the best thing would be that the test env is upgraded to be a vm when a test requires isolation machine

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we always should have our driver selection and behavior deterministic and not automagic.

I.e. if we try running a test which requires vm isolation hint that you can use the lxd driver.
But that one would also need virtualization available on the host which might not necessarily be possible out of the box so automagicness is probably not that desirable here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, and the lxd should get a --vm flag or something


## Exit codes

| Code | Meaning |
|---|---|
| `0` | All tests passed, or skips/no-tests were allowed |
| `1` | Test failure, testbed error, or other autopkgtest error |
| `2` | Strict-only failure: skipped tests or no tests declared under `--strict` |

autopkgtest skips tests whose `Restrictions:` the Environment cannot satisfy (today: `isolation-machine` on every current driver). Skips are reported loudly; use `--strict` to escalate them to exit code 2.

If no `debian/tests/control` exists (or it declares no tests), the run exits 0 with a notice — or exit 2 under `--strict`.

## Inspecting a failed test run

On failure the test environment is torn down by default. Pass `--shell-on-failure` to drop into an interactive shell inside the test environment when stdout is a TTY (destroyed on shell exit unless `--persistent` was used).

Test output and logs are exported to a `test/` subdirectory of the build root; the path is printed at the end of the run.

## Prior build required

By default `debmagic test` resolves the prior build from the build root (same layout as `debmagic shell`). If no build artifacts are found:
run `debmagic build` first

Use `--changes` to supply a `.changes` file from an exported output directory instead.

## Bare driver

The bare driver runs autopkgtest as root directly on the host.
This violates the no-leak principle for normal use — pass `--allow-host-test` to opt in explicitly.
14 changes: 12 additions & 2 deletions packages/debmagic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Modern, robust & easy tooling for building and packaging [Debian](https://debian.org)/[Ubuntu](https://ubuntu.com) packages — while staying backwards compatible.

- **Build any package** in an isolated container environment with `debmagic build`
- **Test and lint** with `debmagic test` and `debmagic check`
- **Run Debian autopkgtest tests** against built packages with `debmagic test`
- **Lint** with `debmagic check`
- **Debug** build environments interactively with `debmagic shell`

## Installation
Expand Down Expand Up @@ -42,6 +43,15 @@ Create a source package (`.dsc`) without compilation:
debmagic build source
```

Run the package's declared Debian autopkgtest tests against a prior build:

```shell
debmagic build binary --driver docker
debmagic test --driver docker
```

Use `--strict` to fail on skipped or undeclared tests (exit code 2). The bare driver requires `--allow-host-test`.

### Useful options

- `--distro <codename>` — select the target distro/release (e.g. `trixie`, `noble`) if the changelog is ambiguous
Expand All @@ -54,7 +64,7 @@ Any of these can be persisted in a `debmagic.toml` config file instead of repeat

### Inspecting a failed build

Failed builds tear down their environment by default. Build with `--persistent` up front, then attach an interactive shell inside the build environment:
Failed builds tear down their environment by default. Pass `--shell-on-failure` to drop into a shell when stdout is a TTY, or build with `--persistent` and attach afterwards:

```shell
debmagic shell
Expand Down
97 changes: 94 additions & 3 deletions packages/debmagic/src/build/artifacts.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::{
ffi::OsStr,
fs,
fs, io,
path::{Component, Path, PathBuf},
};

use anyhow::{Context, anyhow, bail};
use debian_control::lossless::changes::Changes;

fn changes_file_in(build_dir: &Path) -> anyhow::Result<PathBuf> {
/// Locate the single `.changes` file in a build work directory.
pub fn find_changes_file(build_dir: &Path) -> anyhow::Result<PathBuf> {
let mut paths = fs::read_dir(build_dir)
.with_context(|| {
format!(
Expand Down Expand Up @@ -54,7 +55,7 @@ pub fn export_build_artifacts(build_dir: &Path, output_dir: &Path) -> anyhow::Re
fs::create_dir_all(output_dir)
.with_context(|| format!("failed to create output directory {}", output_dir.display()))?;

let changes_path = changes_file_in(build_dir)?;
let changes_path = find_changes_file(build_dir)?;
let changes_metadata = fs::symlink_metadata(&changes_path)?;
if !changes_metadata.file_type().is_file() {
bail!(
Expand Down Expand Up @@ -106,6 +107,74 @@ pub fn export_build_artifacts(build_dir: &Path, output_dir: &Path) -> anyhow::Re
Ok(exported_changes)
}

/// Copy a `.changes` file and every artifact it references into `dest_dir`.
pub fn copy_changes_artifacts(changes_path: &Path, dest_dir: &Path) -> anyhow::Result<()> {
fs::create_dir_all(dest_dir)
.with_context(|| format!("failed to create directory {}", dest_dir.display()))?;

let changes_metadata = fs::symlink_metadata(changes_path)?;
if !changes_metadata.file_type().is_file() {
bail!(
"changes file {} is not a regular file",
changes_path.display()
);
}
let source_dir = changes_path.parent().ok_or_else(|| {
anyhow!(
"changes file {} has no parent directory",
changes_path.display()
)
})?;
let changes = Changes::from_file(changes_path)
.with_context(|| format!("failed to parse {}", changes_path.display()))?;
let files = changes
.files()
.ok_or_else(|| anyhow!("{} has no Files field", changes_path.display()))?;

for file in files {
let filename = artifact_filename(&file.filename)?;
let source = source_dir.join(filename);
let metadata = fs::symlink_metadata(&source).with_context(|| {
format!(
"artifact {} referenced by {} does not exist",
source.display(),
changes_path.display()
)
})?;
if !metadata.file_type().is_file() {
bail!("build artifact {} is not a regular file", source.display());
}
let destination = dest_dir.join(filename);
reject_destination_symlink(&destination)?;
fs::copy(&source, destination)
.with_context(|| format!("failed to copy build artifact {}", source.display()))?;
}

let changes_filename = changes_path
.file_name()
.ok_or_else(|| anyhow!("invalid .changes path: {}", changes_path.display()))?;
let destination = dest_dir.join(changes_filename);
reject_destination_symlink(&destination)?;
fs::copy(changes_path, &destination)
.with_context(|| format!("failed to copy {}", changes_path.display()))?;
Ok(())
}

/// Recursively copy a directory tree. Destination directories are created as needed.
pub(crate) fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let dest_path = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir_all(&entry.path(), &dest_path)?;
} else {
fs::copy(entry.path(), dest_path)?;
}
}
Ok(())
}

#[cfg(test)]
mod tests {
use std::os::unix::fs::symlink;
Expand Down Expand Up @@ -216,4 +285,26 @@ mod tests {
fs::remove_dir_all(build_dir).unwrap();
fs::remove_dir_all(output_dir).unwrap();
}

#[test]
fn copy_dir_all_copies_nested_files() {
let src = test_dir("copy-dir-src");
let dst = test_dir("copy-dir-dst");
fs::create_dir(src.join("nested")).unwrap();
fs::write(src.join("root.txt"), "root").unwrap();
fs::write(src.join("nested").join("child.txt"), "child").unwrap();

copy_dir_all(&src, &dst.join("copied")).unwrap();

assert_eq!(
fs::read_to_string(dst.join("copied").join("root.txt")).unwrap(),
"root"
);
assert_eq!(
fs::read_to_string(dst.join("copied").join("nested").join("child.txt")).unwrap(),
"child"
);
fs::remove_dir_all(src).unwrap();
fs::remove_dir_all(dst).unwrap();
}
}
Loading
Loading