Skip to content
This repository was archived by the owner on Oct 9, 2026. It is now read-only.
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
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ jobs:
exit 1
fi
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
- name: Verify extracted archives without Bun or checkout assets
env:
TARGET: ${{ matrix.target }}
Expand All @@ -57,6 +59,7 @@ jobs:
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"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.target }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ jobs:
bun-version: 1.3.10
- uses: dtolnay/rust-toolchain@1.85.0
- run: cargo build --locked
- run: cargo test --locked --features all-languages --bin diffr --test cli
- working-directory: diffr-ts
run: |
bun install --frozen-lockfile
Expand Down
20 changes: 13 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,38 +94,38 @@ tree-sitter-fortran = { version = "0.6.0", optional = true }
tree-sitter-fsharp = { version = "0.3.0", optional = true }
tree-sitter-gleam = "1.0.0"
tree-sitter-go = "0.25.0"
tree-sitter-haskell = "0.23.1"
tree-sitter-haskell = { version = "0.23.1", optional = true }
tree-sitter-hcl = "1.1.0"
tree-sitter-html = "0.23.2"
tree-sitter-java-orchard = "0.5.9"
tree-sitter-javascript = "0.25.0"
tree-sitter-json = "0.24.8"
tree-sitter-julia = "0.23.1"
tree-sitter-julia = { version = "0.23.1", optional = true }
tree-sitter-lua = "0.5"
tree-sitter-make = "1.1.1"
tree-sitter-newick = "1.1.0"
tree-sitter-nix = "0.3.0"
tree-sitter-objc = "3.0.2"
tree-sitter-ocaml = "0.25.0"
tree-sitter-ocaml = { version = "0.25.0", optional = true }
tree-sitter-pascal = "0.10.0"
tree-sitter-php = "0.24.0"
tree-sitter-proto = "0.4.0"
tree-sitter-python = "0.25.0"
tree-sitter-qmljs = "0.3.0"
tree-sitter-qmljs = { version = "0.3.0", optional = true }
tree-sitter-r = "1.2.0"
tree-sitter-racket = "0.24.7"
tree-sitter-ruby = "0.23.1"
tree-sitter-rust-orchard = "0.16.6"
tree-sitter-scala = "0.26.2"
tree-sitter-scheme = "0.24.7"
tree-sitter-sequel = "0.3.11"
tree-sitter-sfapex = "2.4.0"
tree-sitter-sfapex = { version = "2.4.0", optional = true }
tree-sitter-solidity = "1.2.13"
tree-sitter-swift = "0.7.1"
tree-sitter-toml-ng = "0.7.0"
tree-sitter-typescript = "0.23.2"
tree-sitter-verilog = { version = "1.0.3", optional = true }
tree-sitter-vhdl = "1.4.0"
tree-sitter-vhdl = { version = "1.4.0", optional = true }
tree-sitter-xml = "0.7.0"
tree-sitter-yaml = "0.7.0"
tree-sitter-zig = "1.1.2"
Expand Down Expand Up @@ -235,8 +235,14 @@ name = "wasm"
required-features = ["wasm-plugin-tests"]

[features]
all-languages = ["lang-fortran", "lang-fsharp", "lang-verilog"]
all-languages = ["lang-fortran", "lang-fsharp", "lang-verilog", "lang-ocaml", "lang-julia", "lang-haskell", "lang-vhdl", "lang-apex", "lang-qml"]
lang-fortran = ["dep:tree-sitter-fortran"]
lang-fsharp = ["dep:tree-sitter-fsharp"]
lang-verilog = ["dep:tree-sitter-verilog"]
lang-ocaml = ["dep:tree-sitter-ocaml"]
lang-julia = ["dep:tree-sitter-julia"]
lang-haskell = ["dep:tree-sitter-haskell"]
lang-vhdl = ["dep:tree-sitter-vhdl"]
lang-apex = ["dep:tree-sitter-sfapex"]
lang-qml = ["dep:tree-sitter-qmljs"]
wasm-plugin-tests = []
8 changes: 6 additions & 2 deletions diffr-ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Supports macOS arm64, macOS x64, Linux arm64, and Linux x64. `--check` verifies
network access. Downloads warn on network failure unless `--required`; invalid
hashes or archives always fail.

`--full` fetches the full edition, which adds the parsers lean builds omit (Apex,
Fortran, F#, Haskell, Julia, OCaml, QML, Verilog, VHDL). It requires full-edition
pins, which start with the next release.

## Wire changes

Keep these files in sync:
Expand All @@ -30,8 +34,8 @@ Keep these files in sync:
## Release

After tagging the matching Rust release and building with `cargo build --locked`,
run from `diffr-ts`. New pins select the CLI-only release archives; older pins
continue to use the full archives:
run from `diffr-ts`. New pins select the lean and full CLI-only release archives;
older pins continue to use the combined archives:

```sh
bun install --frozen-lockfile
Expand Down
14 changes: 9 additions & 5 deletions diffr-ts/bin/fetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ try {
async function main() {
const { values } = parseArgs({ options: {
into: { type: "string" },
full: { type: "boolean", default: false },
check: { type: "boolean" },
required: { type: "boolean" },
pins: { type: "string", default: join(root, "pins.json") },
Expand All @@ -30,7 +31,9 @@ async function main() {
if (pins.version !== version) throw new Error(`pins.json is for ${pins.version}, package is ${version}; run \`npm run pin\``);
const target = targets[`${process.platform}-${process.arch}`];
if (!target) throw new Error(`no diffr release for ${process.platform}-${process.arch}`);
const expected = pins.sha256?.[target];
const pin = values.full ? pins.full : pins;
if (!pin) throw new Error(`pins.json has no full edition for ${version}`);
const expected = pin.sha256?.[target];
if (typeof expected !== "string" || !/^[a-f0-9]{64}$/.test(expected)) {
throw new Error(`pins.json has no valid sha256 for ${target}`);
}
Expand All @@ -41,16 +44,17 @@ async function main() {
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() && (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) {
console.log(`diffr ${version} (${target}) already at ${binary}`);
return;
}
if (values.check) throw new Error(`${binary} is missing or not diffr ${version} (${target})`);

const artifact = pins.artifact ?? "diffr";
if (!["diffr", "diffr-cli"].includes(artifact)) throw new Error(`unknown pinned artifact: ${artifact}`);
const artifact = values.full ? "diffr-cli-full" : pins.artifact ?? "diffr";
if (!["diffr", "diffr-cli", "diffr-cli-full"].includes(artifact)) throw new Error(`unknown pinned artifact: ${artifact}`);
const asset = `${artifact}-${version}-${target}.tar.gz`;
const url = `https://github.com/devdotfast/diffr/releases/download/${version}/${asset}`;
let bytes;
Expand Down Expand Up @@ -78,7 +82,7 @@ async function main() {
}
chmodSync(extracted, 0o755);
const stamp = join(staging, "stamp.json");
writeFileSync(stamp, `${JSON.stringify({ version, target }, null, 2)}\n`);
writeFileSync(stamp, `${JSON.stringify({ version, target, ...(values.full && { full: true }) }, null, 2)}\n`);
renameSync(extracted, binary);
renameSync(stamp, stampPath);
} finally {
Expand Down
25 changes: 14 additions & 11 deletions diffr-ts/scripts/pin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ 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 artifact = "diffr-cli";
const sha256 = {};
for (const target of targets) {
const url = `https://github.com/devdotfast/diffr/releases/download/${version}/${artifact}-${version}-${target}.tar.gz`;
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
if (!response.ok) {
console.error(`pin: GET ${url} failed with ${response.status} ${response.statusText}`);
process.exit(1);
async function hashes(artifact) {
const sha256 = {};
for (const target of targets) {
const url = `https://github.com/devdotfast/diffr/releases/download/${version}/${artifact}-${version}-${target}.tar.gz`;
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
if (!response.ok) {
console.error(`pin: GET ${url} failed with ${response.status} ${response.statusText}`);
process.exit(1);
}
sha256[target] = createHash("sha256").update(Buffer.from(await response.arrayBuffer())).digest("hex");
console.log(`${artifact} ${target} ${sha256[target]}`);
}
sha256[target] = createHash("sha256").update(Buffer.from(await response.arrayBuffer())).digest("hex");
console.log(`${target} ${sha256[target]}`);
return sha256;
}
writeFileSync(join(root, "pins.json"), `${JSON.stringify({ version, artifact, sha256 }, null, 2)}\n`);
const pins = { version, artifact: "diffr-cli", sha256: await hashes("diffr-cli"), full: { sha256: await hashes("diffr-cli-full") } };
writeFileSync(join(root, "pins.json"), `${JSON.stringify(pins, null, 2)}\n`);
console.log(`wrote pins.json for ${version}`);
14 changes: 14 additions & 0 deletions diffr-ts/src/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,17 @@ test("unknown pinned artifacts fail before downloading", () => {
expect(f.run().err).toContain("unknown pinned artifact");
expect(existsSync(join(f.dir, "requested"))).toBe(false);
});

test("--full downloads the full archive, fails without full pins, and refetches when switching", () => {
const f = fixture("diffr", "diffr-cli");
expect(f.run(["--full"]).err).toContain("no full edition");
expect(existsSync(join(f.dir, "requested"))).toBe(false);
const pins = JSON.parse(readFileSync(f.pins, "utf8"));
writeFileSync(f.pins, JSON.stringify({ ...pins, full: { sha256: pins.sha256 } }));
expect(f.run(["--full"]).code).toBe(0);
expect(readFileSync(join(f.dir, "requested"), "utf8")).toBe(
`https://github.com/devdotfast/diffr/releases/download/${version}/diffr-cli-full-${version}-${target}.tar.gz`,
);
expect(f.run(["--full", "--check"]).code).toBe(0);
expect(f.run(["--check"]).code).toBe(1);
});
10 changes: 6 additions & 4 deletions diffr-ts/src/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ test("release pins match the package version and cover all supported targets", (
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const pins = JSON.parse(readFileSync(join(root, "pins.json"), "utf8"));
expect(pins.version).toBe(pkg.version);
expect(Object.keys(pins.sha256).sort()).toEqual([
"aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu",
]);
for (const hash of Object.values(pins.sha256)) expect(hash).toMatch(/^[a-f0-9]{64}$/);
for (const { sha256 } of pins.full ? [pins, pins.full] : [pins]) {
expect(Object.keys(sha256).sort()).toEqual([
"aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu",
]);
for (const hash of Object.values(sha256)) expect(hash).toMatch(/^[a-f0-9]{64}$/);
}
});
3 changes: 3 additions & 0 deletions sample_files/ocaml_interface_1.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(** Public interface *)
type result = { value : int }
val compute : int -> result
3 changes: 3 additions & 0 deletions sample_files/ocaml_interface_2.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(** Public interface *)
type result = { value : int; label : string }
val compute : int -> string -> result
9 changes: 5 additions & 4 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ def archive_name(version, target, artifact="diffr"):
return f"{artifact}-{version}-{target}.tar.gz"


def pack(version, target, install, output):
def pack(version, target, install, output, edition="lean"):
actual = subprocess.check_output([install / "bin/diffr", "--version"], text=True).strip()
if actual != f"diffr {version}":
raise ValueError(f"Release version mismatch: {actual}")
for artifact in ("diffr", "diffr-cli"):
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:
Expand All @@ -30,7 +30,7 @@ def pack(version, target, install, output):
def formula(version, output):
checksums = {}
for target in TARGETS:
for artifact in ("diffr", "diffr-cli"):
for artifact in ("diffr", "diffr-cli", "diffr-cli-full"):
name = archive_name(version, target, artifact)
checksums[name] = hashlib.sha256((output / name).read_bytes()).hexdigest()
(output / "SHA256SUMS").write_text("".join(
Expand Down Expand Up @@ -89,13 +89,14 @@ def install
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--target", choices=TARGETS)
parser.add_argument("--install", type=Path)
parser.add_argument("--edition", choices=("lean", "full"), default="lean")
args = parser.parse_args()
if not re.fullmatch(r"\d+\.\d+\.\d+", args.version):
parser.error("version must be a stable X.Y.Z release")
args.output.mkdir(parents=True, exist_ok=True)
if args.command == "pack":
if not args.target or not args.install:
parser.error("pack requires --target and --install")
pack(args.version, args.target, args.install.resolve(), args.output)
pack(args.version, args.target, args.install.resolve(), args.output, args.edition)
else:
formula(args.version, args.output)
Loading
Loading