From b801b31ff92823f63467b150ea7488359d25567a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 18 Apr 2025 13:56:09 +0200 Subject: [PATCH 1/4] feat: generate and append juno:package to WASM custom section --- src/constants/dev.constants.ts | 5 +- src/services/build/build.rust.services.ts | 182 +++++++++++++++++++--- 2 files changed, 165 insertions(+), 22 deletions(-) diff --git a/src/constants/dev.constants.ts b/src/constants/dev.constants.ts index ff825318..7c0247b2 100644 --- a/src/constants/dev.constants.ts +++ b/src/constants/dev.constants.ts @@ -37,7 +37,10 @@ export const RUST_MIN_VERSION = '1.70.0'; export const IC_WASM_MIN_VERSION = '0.8.5'; export const DOCKER_MIN_VERSION = '24.0.0'; -export const DEPLOY_LOCAL_REPLICA_PATH = join(process.cwd(), 'target', 'deploy'); +export const TARGET_PATH = join(process.cwd(), 'target'); +export const DEPLOY_LOCAL_REPLICA_PATH = join(TARGET_PATH, 'deploy'); +export const JUNO_PACKAGE_JSON_PATH = join(TARGET_PATH, 'juno.package.json'); + export const PACKAGE_JSON_PATH = join(process.cwd(), 'package.json'); export const SPUTNIK_INDEX_MJS = 'sputnik.index.mjs'; diff --git a/src/services/build/build.rust.services.ts b/src/services/build/build.rust.services.ts index c6cc1464..29bdfb8d 100644 --- a/src/services/build/build.rust.services.ts +++ b/src/services/build/build.rust.services.ts @@ -1,17 +1,21 @@ -import {nonNullish} from '@dfinity/utils'; +import {isEmptyString, isNullish, nonNullish} from '@dfinity/utils'; import {execute, gzipFile, spawn} from '@junobuild/cli-tools'; +import {type JunoPackage} from '@junobuild/config'; import {generateApi} from '@junobuild/did-tools'; -import {magenta, yellow} from 'kleur'; +import {magenta, red, yellow} from 'kleur'; import {existsSync} from 'node:fs'; import {lstat, mkdir, readFile, rename, writeFile} from 'node:fs/promises'; import {join, relative} from 'node:path'; import ora, {type Ora} from 'ora'; +import {compare, satisfies} from 'semver'; import {detectJunoDevConfigType} from '../../configs/juno.dev.config'; import { DEPLOY_LOCAL_REPLICA_PATH, DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, DEVELOPER_PROJECT_SATELLITE_PATH, - IC_WASM_MIN_VERSION + IC_WASM_MIN_VERSION, + JUNO_PACKAGE_JSON_PATH, + TARGET_PATH } from '../../constants/dev.constants'; import type {BuildArgs} from '../../types/build'; import {readSatelliteDid} from '../../utils/did.utils'; @@ -73,11 +77,20 @@ export const buildRust = async ({path}: Pick = {}) => { }).start(); try { + const buildType = await extractBuildType(); + + if ('error' in buildType) { + console.log(red(buildType.error)); + return; + } + + await prepareJunoPkg({buildType}); + await did(); await didc(); await api(); - await icWasm(); + await icWasm({buildType}); spinner.text = 'Compressing...'; @@ -216,7 +229,110 @@ const api = async () => { }); }; -const icWasm = async () => { +type BuildType = {build: 'legacy'} | {build: 'modern'; version: string; satelliteVersion: string}; + +const prepareJunoPkg = async ({buildType}: {buildType: BuildType}) => { + // We do not write a juno.package.json for legacy build + if (buildType.build === 'legacy') { + return; + } + + const {version, satelliteVersion} = buildType; + + const pkg: JunoPackage = { + version, + name: 'satellite', + dependencies: { + '@junobuild/satellite': satelliteVersion + } + }; + + await writeFile(JUNO_PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 2), 'utf-8'); +}; + +const extractBuildType = async (): Promise => { + await mkdir(TARGET_PATH, {recursive: true}); + + let output = ''; + await spawn({ + command: 'cargo', + args: ['metadata', '--format-version', '1'], + stdout: (o) => (output += o) + }); + + const metadata = JSON.parse(output); + + const satellitedPkg = (metadata?.packages ?? []).find((pkg) => pkg?.name === 'satellite'); + + const version = satellitedPkg?.version; + + if (isNullish(version) || isEmptyString(version)) { + return { + error: 'No version specified. Please add one to the Cargo.toml file of your Satellite.' + }; + } + + const satDependency = (satellitedPkg?.dependencies ?? []).find( + ({name}) => name === 'junobuild-satellite' + ); + + if (isNullish(satDependency)) { + return {error: 'No Satellite dependency. Your project is not a Satellite.'}; + } + + const {req: requiredSatVersion} = satDependency; + + if (isNullish(requiredSatVersion) || isEmptyString(requiredSatVersion)) { + return {error: 'Cannot determine which junobuild-satellite dependency version is required.'}; + } + + const satPackages = (metadata?.packages ?? []).filter( + (pkg) => pkg?.name === 'junobuild-satellite' && satisfies(pkg?.version, requiredSatVersion) + ); + + if (satPackages.length === 0) { + return {error: 'No junobuild-satellite package found in the dependency tree.'}; + } + + // If the developer has multiple crates within the workspace depending on different versions of the junobuild-satellite library. + // This is unusual, as the convention is to have one Satellite per project. + // For now, we throw an error and ask the developer to reach out. + if (satPackages.length > 1) { + return { + error: + 'Multiple junobuild-satellite crates found in the dependency tree. This is not supported at the moment. Please reach out.' + }; + } + + const [satPackage] = satPackages; + + const satelliteVersion = satPackage.metadata?.juno?.satellite?.version; + + if (isNullish(satelliteVersion) || isEmptyString(satelliteVersion)) { + const normalizeVersion = (version: string): string => + version + .trim() + .replace(/^[=^~><]+/, '') // Remove leading =, ^, ~, >, <, >=, <= + .replace(/\s+/, ''); // In case there's a trailing space + + // juno.package.json (used for the WASM custom public section) was introduced after Satellite v0.0.22. + // If the Satellite version is newer, the absence of this metadata is unexpected and we throw an error. + if (compare(normalizeVersion(requiredSatVersion), '0.0.22') > 0) { + return { + error: + 'The metadata required to specify the Satellite version is missing. This is unexpected.' + }; + } + + // For backward compatibility with older versions, we fall back to the legacy ic-wasm approach, + // appending build=extended to the custom section. + return {build: 'legacy'}; + } + + return {build: 'modern', version, satelliteVersion}; +}; + +const icWasm = async ({buildType}: {buildType: BuildType}) => { await mkdir(DEPLOY_LOCAL_REPLICA_PATH, {recursive: true}); // Remove unused functions and debug info. @@ -248,22 +364,46 @@ const icWasm = async () => { ] }); - // Add the type of build "extended" to the satellite. This way, we can identify whether it's the standard canister ("stock") or a custom build of the developer. - await spawn({ - command: 'ic-wasm', - args: [ - SATELLITE_OUTPUT, - '-o', - SATELLITE_OUTPUT, - 'metadata', - 'juno:build', - '-d', - 'extended', - '-v', - 'public', - '--keep-name-section' - ] - }); + // @deprecated + const appendJunoBuild = async () => { + // Add the type of build "extended" to the satellite. This way, we can identify whether it's the standard canister ("stock") or a custom build of the developer. + await spawn({ + command: 'ic-wasm', + args: [ + SATELLITE_OUTPUT, + '-o', + SATELLITE_OUTPUT, + 'metadata', + 'juno:build', + '-d', + 'extended', + '-v', + 'public', + '--keep-name-section' + ] + }); + }; + + const appendJunoPackage = async () => { + await spawn({ + command: 'ic-wasm', + args: [ + SATELLITE_OUTPUT, + '-o', + SATELLITE_OUTPUT, + 'metadata', + 'juno:package', + '-f', + JUNO_PACKAGE_JSON_PATH, + '-v', + 'public', + '--keep-name-section' + ] + }); + }; + + const appendMetadata = buildType.build === 'legacy' ? appendJunoBuild : appendJunoPackage; + await appendMetadata(); // Indicate support for certificate version 1 and 2 in the canister metadata await spawn({ From ce816e9679b242d8fa75e4af74e2ca1f68eaca61 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 18 Apr 2025 13:58:54 +0200 Subject: [PATCH 2/4] chore: lint (minimal) --- src/services/build/build.rust.services.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/build/build.rust.services.ts b/src/services/build/build.rust.services.ts index 29bdfb8d..e66c1eee 100644 --- a/src/services/build/build.rust.services.ts +++ b/src/services/build/build.rust.services.ts @@ -264,7 +264,7 @@ const extractBuildType = async (): Promise => { const satellitedPkg = (metadata?.packages ?? []).find((pkg) => pkg?.name === 'satellite'); - const version = satellitedPkg?.version; + const version: string | null | undefined = satellitedPkg?.version; if (isNullish(version) || isEmptyString(version)) { return { @@ -272,7 +272,7 @@ const extractBuildType = async (): Promise => { }; } - const satDependency = (satellitedPkg?.dependencies ?? []).find( + const satDependency: {req?: string | null | undefined} = (satellitedPkg?.dependencies ?? []).find( ({name}) => name === 'junobuild-satellite' ); @@ -287,7 +287,8 @@ const extractBuildType = async (): Promise => { } const satPackages = (metadata?.packages ?? []).filter( - (pkg) => pkg?.name === 'junobuild-satellite' && satisfies(pkg?.version, requiredSatVersion) + (pkg: {name?: string; version?: string}) => + pkg.name === 'junobuild-satellite' && satisfies(pkg.version ?? '0.0.0', requiredSatVersion) ); if (satPackages.length === 0) { @@ -306,7 +307,7 @@ const extractBuildType = async (): Promise => { const [satPackage] = satPackages; - const satelliteVersion = satPackage.metadata?.juno?.satellite?.version; + const satelliteVersion: string | null | undefined = satPackage.metadata?.juno?.satellite?.version; if (isNullish(satelliteVersion) || isEmptyString(satelliteVersion)) { const normalizeVersion = (version: string): string => From f0221539498271650dae2acbf9d11cafea98f2cc Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 18 Apr 2025 14:21:10 +0200 Subject: [PATCH 3/4] feat: manifest path for metadata --- src/services/build/build.rust.services.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/build/build.rust.services.ts b/src/services/build/build.rust.services.ts index e66c1eee..cfd456bc 100644 --- a/src/services/build/build.rust.services.ts +++ b/src/services/build/build.rust.services.ts @@ -77,7 +77,7 @@ export const buildRust = async ({path}: Pick = {}) => { }).start(); try { - const buildType = await extractBuildType(); + const buildType = await extractBuildType({path}); if ('error' in buildType) { console.log(red(buildType.error)); @@ -250,13 +250,17 @@ const prepareJunoPkg = async ({buildType}: {buildType: BuildType}) => { await writeFile(JUNO_PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 2), 'utf-8'); }; -const extractBuildType = async (): Promise => { +const extractBuildType = async ({path}: Pick = {}): Promise< + BuildType | {error: string} +> => { await mkdir(TARGET_PATH, {recursive: true}); + const manifestArgs = nonNullish(path) ? ['--manifest-path', path] : []; + let output = ''; await spawn({ command: 'cargo', - args: ['metadata', '--format-version', '1'], + args: ['metadata', '--format-version', '1', ...manifestArgs], stdout: (o) => (output += o) }); From 260b62e887f72b25cc34c36b78079c00c77d47d7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 18 Apr 2025 14:23:38 +0200 Subject: [PATCH 4/4] feat: extract const --- src/services/build/build.rust.services.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/services/build/build.rust.services.ts b/src/services/build/build.rust.services.ts index cfd456bc..8fdd63ce 100644 --- a/src/services/build/build.rust.services.ts +++ b/src/services/build/build.rust.services.ts @@ -26,6 +26,7 @@ import {confirmAndExit} from '../../utils/prompt.utils'; const CARGO_RELEASE_DIR = join(process.cwd(), 'target', 'wasm32-unknown-unknown', 'release'); const SATELLITE_OUTPUT = join(DEPLOY_LOCAL_REPLICA_PATH, 'satellite.wasm'); +const SATELLITE_PROJECT_NAME = 'satellite'; export const buildRust = async ({path}: Pick = {}) => { const {valid: validRust} = await checkRustVersion(); @@ -52,7 +53,7 @@ export const buildRust = async ({path}: Pick = {}) => { return; } - const defaultProjectArgs = ['-p', 'satellite']; + const defaultProjectArgs = ['-p', SATELLITE_PROJECT_NAME]; const args = [ 'build', @@ -241,7 +242,7 @@ const prepareJunoPkg = async ({buildType}: {buildType: BuildType}) => { const pkg: JunoPackage = { version, - name: 'satellite', + name: SATELLITE_PROJECT_NAME, dependencies: { '@junobuild/satellite': satelliteVersion } @@ -266,7 +267,9 @@ const extractBuildType = async ({path}: Pick = {}): Promise< const metadata = JSON.parse(output); - const satellitedPkg = (metadata?.packages ?? []).find((pkg) => pkg?.name === 'satellite'); + const satellitedPkg = (metadata?.packages ?? []).find( + (pkg) => pkg?.name === SATELLITE_PROJECT_NAME + ); const version: string | null | undefined = satellitedPkg?.version;