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: 4 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[alias]
xtask = "run --package xtask --"

# Release binaries must run on Windows installs without the Visual C++ runtime.
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
32 changes: 20 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ jobs:
os: ubuntu-22.04
- target: aarch64-unknown-linux-gnu
os: ubuntu-22.04-arm
- target: x86_64-pc-windows-msvc
os: windows-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 45
defaults:
run:
shell: bash
env:
TARGET: ${{ matrix.target }}
PYTHON: ${{ startsWith(matrix.os, 'windows') && 'python' || 'python3' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
Expand All @@ -38,28 +46,28 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- name: Build and package
env:
TARGET: ${{ matrix.target }}
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "diffr-cli") | .version')
version=$(cargo metadata --no-deps --format-version 1 | "$PYTHON" -c 'import json, sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"] == "diffr-cli"), end="")')
if [ -n "$RELEASE_TAG" ] && [ "$RELEASE_TAG" != "$version" ]; then
echo "Tag $RELEASE_TAG does not match Cargo version $version" >&2
exit 1
fi
echo "VERSION=$version" >> "$GITHUB_ENV"
cargo xtask install --root "$RUNNER_TEMP/install"
cargo install --path . --locked --features all-languages --root "$RUNNER_TEMP/full"
python3 scripts/release.py pack --version "$version" --target "$TARGET" --install "$RUNNER_TEMP/install" --output dist
python3 scripts/release.py pack --version "$version" --target "$TARGET" --install "$RUNNER_TEMP/full" --edition full --output dist
"$PYTHON" scripts/release.py pack --version "$version" --target "$TARGET" --install "$RUNNER_TEMP/install" --output dist
"$PYTHON" scripts/release.py pack --version "$version" --target "$TARGET" --install "$RUNNER_TEMP/full" --edition full --output dist
- name: Verify extracted archives without Bun or checkout assets
env:
TARGET: ${{ matrix.target }}
run: |
version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
mkdir -p "$RUNNER_TEMP/extracted/bin"
tar -xzf "dist/diffr-$version-$TARGET.tar.gz" -C "$RUNNER_TEMP/extracted/bin"
python3 xtask/tests/smoke_install.py "$RUNNER_TEMP/extracted"
python3 scripts/smoke_cli_archive.py "dist/diffr-cli-$version-$TARGET.tar.gz" "$version"
python3 scripts/smoke_cli_archive.py "dist/diffr-cli-full-$version-$TARGET.tar.gz" "$version"
# The TUI smoke test drives a Unix PTY.
if [ "$RUNNER_OS" != Windows ]; then
mkdir -p "$RUNNER_TEMP/extracted/bin"
tar -xzf "dist/diffr-$VERSION-$TARGET.tar.gz" -C "$RUNNER_TEMP/extracted/bin"
"$PYTHON" xtask/tests/smoke_install.py "$RUNNER_TEMP/extracted"
fi
"$PYTHON" scripts/smoke_cli_archive.py "dist/diffr-cli-$VERSION-$TARGET.tar.gz" "$VERSION"
"$PYTHON" scripts/smoke_cli_archive.py "dist/diffr-cli-full-$VERSION-$TARGET.tar.gz" "$VERSION"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.target }}
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ cargo binstall diffr-cli
cargo install diffr-cli --locked
```

Windows x64: download `diffr-<version>-<target>.tar.gz` from [Releases](https://github.com/devdotfast/diffr/releases) and extract it with `tar -xzf`, or use `cargo binstall`.

From source: `cargo xtask install` (requires Rust and [Bun](https://bun.sh)).

## History
Expand Down
2 changes: 1 addition & 1 deletion diffr-ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const event = decodeStructuralDiffEvent(line);
npx --package @dev.fast/diffr@0.1.6 diffr-fetch --into ./bin --required
```

Supports macOS arm64, macOS x64, Linux arm64, and Linux x64. `--check` verifies an existing install without
Supports macOS arm64, macOS x64, Linux arm64, Linux x64, and Windows x64 (`diffr.exe`). `--check` verifies an existing install without
network access. Downloads warn on network failure unless `--required`; invalid
hashes or archives always fail.

Expand Down
17 changes: 9 additions & 8 deletions diffr-ts/bin/fetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const targets = { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu" };
const targets = { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu", "win32-x64": "x86_64-pc-windows-msvc" };

try {
await main();
Expand Down Expand Up @@ -38,13 +38,14 @@ async function main() {
throw new Error(`pins.json has no valid sha256 for ${target}`);
}

const binary = join(into, "diffr");
const name = process.platform === "win32" ? "diffr.exe" : "diffr";
const binary = join(into, name);
const stampPath = join(into, "diffr.stamp.json");
let installed = false;
try {
const stamp = JSON.parse(readFileSync(stampPath, "utf8"));
const stat = lstatSync(binary);
installed = stat.isFile() && (stat.mode & 0o111) !== 0 && stamp.version === version && stamp.target === target
installed = stat.isFile() && (process.platform === "win32" || (stat.mode & 0o111) !== 0) && stamp.version === version && stamp.target === target
&& Boolean(stamp.full) === values.full;
} catch { /* Missing or damaged installations are fetched again. */ }
if (installed) {
Expand Down Expand Up @@ -73,12 +74,12 @@ async function main() {
mkdirSync(into, { recursive: true });
const staging = mkdtempSync(join(into, ".diffr-"));
try {
const archive = join(staging, "archive.tar.gz");
writeFileSync(archive, bytes);
const tar = spawnSync("tar", ["-xzf", archive, "-C", staging, "diffr"], { encoding: "utf8" });
const extracted = join(staging, "diffr");
writeFileSync(join(staging, "archive.tar.gz"), bytes);
// Relative paths: GNU tar on Windows reads "C:" in an archive path as a remote host.
const tar = spawnSync("tar", ["-xzf", "archive.tar.gz", name], { cwd: staging, encoding: "utf8" });
const extracted = join(staging, name);
if (tar.status !== 0 || !lstatSync(extracted).isFile()) {
throw new Error(`${asset} does not contain a regular diffr file at its root`);
throw new Error(`${asset} does not contain a regular ${name} file at its root`);
}
chmodSync(extracted, 0o755);
const stamp = join(staging, "stamp.json");
Expand Down
2 changes: 1 addition & 1 deletion diffr-ts/scripts/pin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, "..");
const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
const targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"];
const targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-pc-windows-msvc"];
async function hashes(artifact) {
const sha256 = {};
for (const target of targets) {
Expand Down
14 changes: 13 additions & 1 deletion diffr-ts/src/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { join } from "node:path";

const fetchScript = join(import.meta.dir, "..", "bin", "fetch.mjs");
const { version } = JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8"));
const targets: Record<string, string> = { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu" };
const targets: Record<string, string> = { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu", "win32-x64": "x86_64-pc-windows-msvc" };
const target = targets[`${process.platform}-${process.arch}`];
const dirs: string[] = [];
afterEach(() => {
Expand Down Expand Up @@ -137,6 +137,18 @@ test("Linux ARM64 downloads and stamps the aarch64 release", () => {
expect(f.run(["--check"], true).code).toBe(0);
});

test("Windows x64 downloads and stamps diffr.exe", () => {
const f = fixture("diffr.exe", "diffr-cli", "win32", "x64");
const result = f.run(["--required"]);
expect(result.code, result.err).toBe(0);
expect(readFileSync(join(f.dir, "requested"), "utf8")).toBe(
`https://github.com/devdotfast/diffr/releases/download/${version}/diffr-cli-${version}-x86_64-pc-windows-msvc.tar.gz`,
);
expect(readdirSync(f.into).sort()).toEqual(["diffr.exe", "diffr.stamp.json"]);
chmodSync(join(f.into, "diffr.exe"), 0o644);
expect(f.run(["--check"], true).code).toBe(0);
});

test("CLI-only pins download the CLI archive", () => {
const f = fixture("diffr", "diffr-cli");
const result = f.run(["--required"]);
Expand Down
10 changes: 7 additions & 3 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import subprocess
import tarfile

TARGETS = ("aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu")
TARGETS = (
"aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
)
ROOT = Path(__file__).resolve().parent.parent


Expand All @@ -15,14 +18,15 @@ def archive_name(version, target, artifact="diffr"):


def pack(version, target, install, output, edition="lean"):
actual = subprocess.check_output([install / "bin/diffr", "--version"], text=True).strip()
exe = ".exe" if "windows" in target else ""
actual = subprocess.check_output([install / "bin" / f"diffr{exe}", "--version"], text=True).strip()
if actual != f"diffr {version}":
raise ValueError(f"Release version mismatch: {actual}")
for artifact in ("diffr-cli-full",) if edition == "full" else ("diffr", "diffr-cli"):
with tarfile.open(output / archive_name(version, target, artifact), "w:gz") as archive:
binaries = ("diffr", "diffr-tui") if artifact == "diffr" else ("diffr",)
for name in binaries:
archive.add(install / "bin" / name, arcname=name)
archive.add(install / "bin" / f"{name}{exe}", arcname=f"{name}{exe}")
for name in ("LICENSE", "NOTICE", "tui/LICENSE", "tui/themes/LICENSE"):
archive.add(ROOT / name, arcname=name)

Expand Down
8 changes: 6 additions & 2 deletions scripts/smoke_cli_archive.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Verify a CLI-only release archive without Bun, Git, or user configuration."""
import json
import os
from pathlib import Path
import subprocess
import sys
Expand All @@ -8,18 +9,21 @@


def check(archive_path, version):
exe = ".exe" if os.name == "nt" else ""
with tempfile.TemporaryDirectory(prefix="diffr-cli-smoke-") as directory:
root = Path(directory)
with tarfile.open(archive_path) as archive:
assert set(archive.getnames()) == {
"diffr", "LICENSE", "NOTICE", "tui/LICENSE", "tui/themes/LICENSE",
f"diffr{exe}", "LICENSE", "NOTICE", "tui/LICENSE", "tui/themes/LICENSE",
}
archive.extractall(root, filter="data")
env = {"PATH": "", "HOME": str(root), "XDG_CONFIG_HOME": str(root / "config")}
if os.name == "nt":
env["SYSTEMROOT"] = os.environ["SYSTEMROOT"]

def run(*args):
return subprocess.check_output(
[root / "diffr", *args], cwd=root, env=env, text=True, timeout=30,
[root / f"diffr{exe}", *args], cwd=root, env=env, text=True, timeout=30,
)

assert run("--version").strip() == f"diffr {version}"
Expand Down
Loading