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
5 changes: 5 additions & 0 deletions .changeset/shiny-tables-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/devnet": patch
---

Refactor anvil version detection by normalizing the stdout and recovering a clean semver of the normalized object. Throws errors to provide feedback accordingly.
1 change: 1 addition & 0 deletions bun.lock

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

85 changes: 75 additions & 10 deletions packages/devnet/anvil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
import { $ } from "bun";
import pRetry from "p-retry";
import { SemVer } from "semver";

/**
* Represents the output of the `anvil --version` command after parsing it into a structured format.
* Type defined based on anvil version 1.4.3-v1.4.3 output.
*
* @example
* // anvil Version: 1.4.3-v1.4.3
* // Commit SHA: fa9f934bdac4bcf57e694e852a61997dda90668a
* // Build Timestamp: 2025-10-22T04:37:38.758664000Z (1761107858)
* // Build Profile: maxperf
*
*/
type AnvilVersionStdout = {
anvil_version: string;
commit_sha: string;
build_timestamp: string;
build_profile: string;
};

type AnvilVersionStdoutKeys = keyof AnvilVersionStdout;

const normalizeAnvilVersionOutput = (output: string) => {
return output.split("\n").reduce(
(curr, next) => {
const trimmedLine = next.trim();
const firstColonIndex = trimmedLine.indexOf(":");

if (firstColonIndex === -1) return curr;

const prop = trimmedLine.slice(0, firstColonIndex);
const value = trimmedLine.slice(firstColonIndex + 1);
const key = prop
.trim()
.toLowerCase()
.replace(/\s+/g, "_") as AnvilVersionStdoutKeys;

if (key && value) {
curr[key] = value.trim();
}

return curr;
},
{} as Partial<AnvilVersionStdout>,
);
};

/**
* Get the clean version of an anvil version string.
* Anvil uses semver with a quirk by suffixing the version with tag-names e.g. 1.4.3-v1.4.3,
* so we need to parse it with semver and get the clean version.
* @param anvilVersion
* @returns
*/
const getCleanVersion = (anvilVersion: string) => {
try {
const semver = new SemVer(anvilVersion, { loose: true });
return `${semver.major}.${semver.minor}.${semver.patch}`;
} catch {
// If the version is not a valid semver, return null
return null;
}
};

/**
* Get the installed anvil version
Expand All @@ -8,19 +71,21 @@ import pRetry from "p-retry";
export const version = async () => {
const output = await $`anvil --version`.text();

// anvil Version: 1.4.3-v1.4.3
// Commit SHA: fa9f934bdac4bcf57e694e852a61997dda90668a
// Build Timestamp: 2025-10-22T04:37:38.758664000Z (1761107858)
// Build Profile: maxperf
const data = normalizeAnvilVersionOutput(output);

// parse the output to get the version
const versionMatch = output.match(
/Version: (\d+\.\d+\.\d+)-v(\d+\.\d+\.\d+)/,
);
if (!versionMatch) {
if (!data.anvil_version) {
throw new Error("Failed to parse anvil version. Is anvil installed?");
}
return versionMatch[1];

const cleanVersion = getCleanVersion(data.anvil_version);

if (!cleanVersion) {
throw new Error(
`Anvil version "${data.anvil_version}" is not a valid semver.`,
);
}

return cleanVersion;
};

type StartOptions = {
Expand Down
1 change: 1 addition & 0 deletions packages/devnet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"devDependencies": {
"@types/bun": "^1.3.9",
"@types/fs-extra": "^11.0.4",
"@types/semver": "^7.7.1",
"fs-extra": "^11.3.2",
"listr2": "^10.1.0",
"modern-tar": "^0.7.3",
Expand Down
Loading