diff --git a/noir/compiler/wasm/.gitignore b/noir/compiler/wasm/.gitignore index f968dafbcc38..472e22078be6 100644 --- a/noir/compiler/wasm/.gitignore +++ b/noir/compiler/wasm/.gitignore @@ -1 +1,2 @@ noir-script/target +dist diff --git a/yarn-project/noir-compiler/src/cli/add_noir_compiler_commander_actions.ts b/yarn-project/noir-compiler/src/cli/add_noir_compiler_commander_actions.ts index 7c756965188e..4c1dd616c203 100644 --- a/yarn-project/noir-compiler/src/cli/add_noir_compiler_commander_actions.ts +++ b/yarn-project/noir-compiler/src/cli/add_noir_compiler_commander_actions.ts @@ -3,20 +3,6 @@ import { LogFn } from '@aztec/foundation/log'; import { Command } from 'commander'; import { dirname } from 'path'; -/** - * CLI options for configuring behavior - */ -interface Options { - // eslint-disable-next-line jsdoc/require-jsdoc - outdir: string; - // eslint-disable-next-line jsdoc/require-jsdoc - typescript: string | undefined; - // eslint-disable-next-line jsdoc/require-jsdoc - interface: string | undefined; - // eslint-disable-next-line jsdoc/require-jsdoc - compiler: string | undefined; -} - /** * */ @@ -24,25 +10,6 @@ export function addNoirCompilerCommanderActions(program: Command, log: LogFn = ( addCodegenCommanderAction(program, log); } -/** - * - */ -export function addCompileCommanderAction(program: Command, log: LogFn = () => {}) { - program - .command('compile') - .argument('', 'Path to the bin or Aztec.nr project to compile') - .option('-o, --outdir ', 'Output folder for the binary artifacts, relative to the project path', 'target') - .option('-ts, --typescript ', 'Optional output folder for generating typescript wrappers', undefined) - .option('-i, --interface ', 'Optional output folder for generating an Aztec.nr contract interface', undefined) - .option('-c --compiler ', 'Which compiler to use. Either nargo or wasm.', 'wasm') - .description('Compiles the Noir Source in the target project') - - .action(async (projectPath: string, options: Options) => { - const { compileNoir } = await import('./compile_noir.js'); - await compileNoir(projectPath, options, log); - }); -} - /** * */ diff --git a/yarn-project/noir-compiler/src/cli/compile_noir.ts b/yarn-project/noir-compiler/src/cli/compile_noir.ts deleted file mode 100644 index f87055464a95..000000000000 --- a/yarn-project/noir-compiler/src/cli/compile_noir.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { ContractArtifact } from '@aztec/foundation/abi'; -import { LogFn } from '@aztec/foundation/log'; - -import { mkdirSync, writeFileSync } from 'fs'; -import { mkdirpSync } from 'fs-extra'; -import path, { resolve } from 'path'; - -import { - ProgramArtifact, - compileUsingNargo, - compileUsingNoirWasm, - generateNoirContractInterface, - generateTypescriptContractInterface, - generateTypescriptProgramInterface, -} from '../index.js'; - -/** - * CLI options for configuring behavior - */ -interface Options { - // eslint-disable-next-line jsdoc/require-jsdoc - outdir: string; - // eslint-disable-next-line jsdoc/require-jsdoc - typescript: string | undefined; - // eslint-disable-next-line jsdoc/require-jsdoc - interface: string | undefined; - // eslint-disable-next-line jsdoc/require-jsdoc - compiler: string | undefined; -} -/** - * Registers a 'contract' command on the given commander program that compiles an Aztec.nr contract project. - * @param program - Commander program. - * @param log - Optional logging function. - * @returns The program with the command registered. - */ -export async function compileNoir(projectPath: string, options: Options, log: LogFn = () => {}) { - const { compiler } = options; - if (typeof projectPath !== 'string') { - throw new Error(`Missing project path argument`); - } - if (compiler !== 'nargo' && compiler !== 'wasm') { - throw new Error(`Invalid compiler: ${compiler}`); - } - - const compile = compiler === 'wasm' ? compileUsingNoirWasm : compileUsingNargo; - log(`Compiling ${projectPath} with ${compiler} backend...`); - const results = await compile(projectPath, { log }); - for (const result of results) { - generateOutput(projectPath, result, options, log); - } -} - -/** - * - * @param contract - output from compiler, to serialize locally. branch based on Contract vs Program - */ -function generateOutput( - projectPath: string, - _result: ContractArtifact | ProgramArtifact, - options: Options, - log: LogFn, -) { - const contract = _result as ContractArtifact; - if (contract.name) { - return generateContractOutput(projectPath, contract, options, log); - } else { - const program = _result as ProgramArtifact; - if (program.abi) { - return generateProgramOutput(projectPath, program, options, log); - } - } -} -/** - * - * @param program - output from compiler, to serialize locally - */ -function generateProgramOutput(projectPath: string, program: ProgramArtifact, options: Options, log: LogFn) { - const currentDir = process.cwd(); - const { outdir, typescript, interface: noirInterface } = options; - const artifactPath = resolve(projectPath, outdir, `${program.name ? program.name : 'main'}.json`); - log(`Writing ${program.name} artifact to ${path.relative(currentDir, artifactPath)}`); - mkdirSync(path.dirname(artifactPath), { recursive: true }); - writeFileSync(artifactPath, JSON.stringify(program, null, 2)); - - if (noirInterface) { - log(`noirInterface generation not implemented for programs`); - // not implemented - } - - if (typescript) { - // just need type definitions, since a lib has just one entry point - const tsPath = resolve(projectPath, typescript, `../types/${program.name}_types.ts`); - log(`Writing ${program.name} typescript types to ${path.relative(currentDir, tsPath)}`); - const tsWrapper = generateTypescriptProgramInterface(program.abi); - mkdirpSync(path.dirname(tsPath)); - writeFileSync(tsPath, tsWrapper); - } -} - -/** - * - * @param contract - output from compiler, to serialize locally - */ -function generateContractOutput(projectPath: string, contract: ContractArtifact, options: Options, log: LogFn) { - const currentDir = process.cwd(); - const { outdir, typescript, interface: noirInterface } = options; - const artifactPath = resolve(projectPath, outdir, `${contract.name}.json`); - log(`Writing ${contract.name} artifact to ${path.relative(currentDir, artifactPath)}`); - mkdirSync(path.dirname(artifactPath), { recursive: true }); - writeFileSync(artifactPath, JSON.stringify(contract, null, 2)); - - if (noirInterface) { - const noirInterfacePath = resolve(projectPath, noirInterface, `${contract.name}_interface.nr`); - log(`Writing ${contract.name} Aztec.nr external interface to ${path.relative(currentDir, noirInterfacePath)}`); - const noirWrapper = generateNoirContractInterface(contract); - mkdirpSync(path.dirname(noirInterfacePath)); - writeFileSync(noirInterfacePath, noirWrapper); - } - - if (typescript) { - const tsPath = resolve(projectPath, typescript, `${contract.name}.ts`); - log(`Writing ${contract.name} typescript interface to ${path.relative(currentDir, tsPath)}`); - let relativeArtifactPath = path.relative(path.dirname(tsPath), artifactPath); - if (relativeArtifactPath === `${contract.name}.json`) { - // relative path edge case, prepending ./ for local import - the above logic just does - // `${contract.name}.json`, which is not a valid import for a file in the same directory - relativeArtifactPath = `./${contract.name}.json`; - } - const tsWrapper = generateTypescriptContractInterface(contract, relativeArtifactPath); - mkdirpSync(path.dirname(tsPath)); - writeFileSync(tsPath, tsWrapper); - } -} diff --git a/yarn-project/noir-compiler/src/compile/nargo.ts b/yarn-project/noir-compiler/src/compile/nargo.ts deleted file mode 100644 index f1367b0930d1..000000000000 --- a/yarn-project/noir-compiler/src/compile/nargo.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { LogFn, createDebugLogger } from '@aztec/foundation/log'; - -import { execSync } from 'child_process'; -import { emptyDir } from 'fs-extra'; -import { readFile, readdir, stat, unlink } from 'fs/promises'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -import { NoirCommit, NoirTag } from '../index.js'; -import { NoirCompiledContract, NoirContractCompilationArtifacts, NoirDebugMetadata } from '../noir_artifact.js'; - -/** Compilation options */ -export type CompileOpts = { - /** Silence output from nargo compile. */ - quiet?: boolean; - /** Path to the nargo binary. */ - nargoBin?: string; - /** Logging function */ - log?: LogFn; -}; - -/** - * - */ -function getCurrentDir() { - if (typeof __dirname !== 'undefined') { - return __dirname; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return dirname(fileURLToPath(import.meta.url)); - } -} - -/** - * A class that compiles Aztec.nr contracts using nargo via the shell. - */ -export class NargoContractCompiler { - private log: LogFn; - constructor(private projectPath: string, private opts: CompileOpts = {}) { - this.log = opts.log ?? createDebugLogger('aztec:noir-compiler'); - } - - /** - * Compiles the contracts in projectPath and returns the Aztec.nr artifact. - * @returns Aztec.nr artifact of the compiled contracts. - */ - public async compile(): Promise { - const stdio = this.opts.quiet ? 'ignore' : 'inherit'; - const nargoBin = this.opts.nargoBin ?? getCurrentDir() + '/../../../../noir/target/release/nargo'; - const version = execSync(`${nargoBin} --version`, { cwd: this.projectPath, stdio: 'pipe' }).toString(); - this.checkNargoBinVersion(version.replace('\n', '')); - await emptyDir(this.getTargetFolder()); - execSync(`${nargoBin} compile`, { cwd: this.projectPath, stdio }); - return Promise.resolve(this.collectArtifacts()); - } - - private checkNargoBinVersion(version: string) { - if (!version.includes(NoirCommit)) { - this.log( - `Warning: the nargo version installed locally does not match the expected one. This may cause issues when compiling or deploying contracts. Consider updating your nargo or aztec-cli installation. \n- Expected: ${NoirTag} (git version hash: ${NoirCommit})\n- Found: ${version}`, - ); - } else if (!this.opts.quiet) { - this.log(`Using ${version}`); - } - } - - private async collectArtifacts(): Promise { - const contractArtifacts = new Map(); - const debugArtifacts = new Map(); - - for (const filename of await readdir(this.getTargetFolder())) { - const file = join(this.getTargetFolder(), filename); - if ((await stat(file)).isFile() && file.endsWith('.json')) { - if (filename.startsWith('debug_')) { - debugArtifacts.set( - filename.replace('debug_', ''), - JSON.parse((await readFile(file)).toString()) as NoirDebugMetadata, - ); - } else { - contractArtifacts.set(filename, JSON.parse((await readFile(file)).toString()) as NoirCompiledContract); - } - // Delete the file as it is not needed anymore and it can cause issues with prettier - await unlink(file); - } - } - - return [...contractArtifacts.entries()].map(([filename, contractArtifact]) => { - const debugArtifact = debugArtifacts.get(filename); - return { - contract: contractArtifact, - debug: debugArtifact, - }; - }); - } - - private getTargetFolder() { - return join(this.projectPath, 'target'); - } -} diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-manager.test.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-manager.test.ts deleted file mode 100644 index 5f9ba570d4d1..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-manager.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { NoirDependencyConfig } from '@aztec/foundation/noir'; - -import { NoirPackage } from '../package.js'; -import { NoirDependencyManager } from './dependency-manager.js'; -import { NoirDependency, NoirDependencyResolver } from './dependency-resolver.js'; - -describe('DependencyManager', () => { - let manager: NoirDependencyManager; - - beforeEach(() => { - manager = new NoirDependencyManager( - [new TestDependencyResolver()], - new NoirPackage('/test_contract', '/test_contract/src', { - dependencies: { - lib1: { - path: '/lib1', - }, - lib2: { - path: '/lib2', - }, - lib3: { - path: '/lib3', - }, - }, - package: { - name: 'test_contract', - type: 'contract', - }, - }), - ); - }); - - it('successfully resolves dependencies', async () => { - await expect(manager.resolveDependencies()).resolves.toBeUndefined(); - }); - - it('resolves all libraries', async () => { - await manager.resolveDependencies(); - expect(manager.getPackageNames()).toEqual(['lib1', 'lib2', 'lib3']); - }); - - it('resolves root dependencies', async () => { - await manager.resolveDependencies(); - expect(manager.getEntrypointDependencies()).toEqual(['lib1', 'lib2', 'lib3']); - }); - - it('resolves library dependencies', async () => { - await manager.resolveDependencies(); - expect(manager.getLibraryDependencies()).toEqual({ - lib2: ['lib3'], - }); - }); -}); - -class TestDependencyResolver implements NoirDependencyResolver { - // eslint-disable-next-line require-await - public async resolveDependency(pkg: NoirPackage, dep: NoirDependencyConfig): Promise { - if (!('path' in dep)) { - return null; - } - - switch (dep.path) { - case '/lib1': - return { - version: '', - package: new NoirPackage('/lib1', '/lib1/src', { - dependencies: {}, - package: { - name: 'lib1', - type: 'lib', - }, - }), - }; - - case '/lib2': - return { - version: '', - package: new NoirPackage('/lib2', '/lib2/src', { - dependencies: { - lib3: { - path: '../lib3', - }, - }, - package: { - name: 'lib2', - type: 'lib', - }, - }), - }; - - case '/lib3': - return { - version: '', - package: new NoirPackage('/lib3', '/lib3/src', { - dependencies: {}, - package: { - name: 'lib3', - type: 'lib', - }, - }), - }; - - default: - throw new Error(); - } - } -} diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-manager.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-manager.ts deleted file mode 100644 index cc4fc179d44f..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-manager.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { LogFn, createDebugOnlyLogger } from '@aztec/foundation/log'; -import { NoirDependencyConfig } from '@aztec/foundation/noir'; - -import { join } from 'node:path'; - -import { NoirPackage } from '../package.js'; -import { NoirDependency, NoirDependencyResolver } from './dependency-resolver.js'; - -/** - * Noir Dependency Resolver - */ -export class NoirDependencyManager { - #entryPoint: NoirPackage; - #libraries = new Map(); - #dependencies = new Map(); - #log: LogFn; - #resolvers: readonly NoirDependencyResolver[]; - - /** - * Creates a new dependency resolver - * @param resolvers - A list of dependency resolvers to use - * @param entryPoint - The entry point of the project - */ - constructor(resolvers: readonly NoirDependencyResolver[] = [], entryPoint: NoirPackage) { - this.#log = createDebugOnlyLogger('noir:dependency-resolver'); - this.#resolvers = resolvers; - this.#entryPoint = entryPoint; - } - - /** - * Gets dependencies for the entry point - */ - public getEntrypointDependencies() { - return this.#dependencies.get('') ?? []; - } - - /** - * Get transitive libraries used by the package - */ - public getLibraries() { - return Array.from(this.#libraries.entries()); - } - - /** - * A map of library dependencies - */ - public getLibraryDependencies() { - const entries = Array.from(this.#dependencies.entries()); - return Object.fromEntries(entries.filter(([name]) => name !== '')); - } - - /** - * Resolves dependencies for a package. - */ - public async resolveDependencies(): Promise { - await this.#breadthFirstResolveDependencies(); - } - - /** - * Gets the version of a dependency in the dependency tree - * @param name - Dependency name - * @returns The dependency's version - */ - public getVersionOf(name: string): string | undefined { - const dep = this.#libraries.get(name); - return dep?.version; - } - - async #breadthFirstResolveDependencies(): Promise { - /** Represents a package to resolve dependencies for */ - type Job = { - /** Package name */ - packageName: string; - /** The package location */ - noirPackage: NoirPackage; - }; - - const queue: Job[] = [ - { - packageName: '', - noirPackage: this.#entryPoint, - }, - ]; - - while (queue.length > 0) { - const { packageName, noirPackage } = queue.shift()!; - for (const [name, config] of Object.entries(noirPackage.getDependencies())) { - // TODO what happens if more than one package has the same name but different versions? - if (this.#libraries.has(name)) { - this.#log(`skipping already resolved dependency ${name}`); - this.#dependencies.set(packageName, [...(this.#dependencies.get(packageName) ?? []), name]); - - continue; - } - const dependency = await this.#resolveDependency(noirPackage, config); - if (dependency.package.getType() !== 'lib') { - this.#log(`Non-library package ${name}`, config); - throw new Error(`Dependency ${name} is not a library`); - } - - this.#libraries.set(name, dependency); - this.#dependencies.set(packageName, [...(this.#dependencies.get(packageName) ?? []), name]); - - queue.push({ - noirPackage: dependency.package, - packageName: name, - }); - } - } - } - - async #resolveDependency(pkg: NoirPackage, config: NoirDependencyConfig): Promise { - let dependency: NoirDependency | null = null; - for (const resolver of this.#resolvers) { - dependency = await resolver.resolveDependency(pkg, config); - if (dependency) { - break; - } - } - - if (!dependency) { - throw new Error('Dependency not resolved'); - } - - return dependency; - } - - /** - * Gets the names of the crates in this dependency list - */ - public getPackageNames() { - return [...this.#libraries.keys()]; - } - - /** - * Looks up a dependency - * @param sourceId - The source being resolved - * @returns The path to the resolved file - */ - public findFile(sourceId: string): string | null { - const [lib, ...path] = sourceId.split('/').filter(x => x); - const dep = this.#libraries.get(lib); - if (dep) { - return join(dep.package.getSrcPath(), ...path); - } else { - return null; - } - } -} diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-resolver.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-resolver.ts deleted file mode 100644 index f8a955aa2c7b..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/dependency-resolver.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NoirDependencyConfig } from '@aztec/foundation/noir'; - -import { NoirPackage } from '../package.js'; - -/** - * A Noir dependency - */ -export type NoirDependency = { - /** version string as determined by the resolver */ - version?: string; - /** the actual package source code */ - package: NoirPackage; -}; - -/** - * Resolves a dependency for a package. - */ -export interface NoirDependencyResolver { - /** - * Resolve a dependency for a package. - * @param pkg - The package to resolve dependencies for - * @param dep - The dependency config to resolve - */ - resolveDependency(pkg: NoirPackage, dep: NoirDependencyConfig): Promise; -} diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/github-dependency-resolver.test.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/github-dependency-resolver.test.ts deleted file mode 100644 index d171a6ccf6b1..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/github-dependency-resolver.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { NoirGitDependencyConfig } from '@aztec/foundation/noir'; -import { fileURLToPath } from '@aztec/foundation/url'; - -import { jest } from '@jest/globals'; -import { Volume, createFsFromVolume } from 'memfs'; -import { readFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; - -import { FileManager } from '../file-manager/file-manager.js'; -import { createMemFSFileManager } from '../file-manager/memfs-file-manager.js'; -import { NoirPackage } from '../package.js'; -import { NoirDependencyResolver } from './dependency-resolver.js'; -import { GithubDependencyResolver, resolveGithubCodeArchive, safeFilename } from './github-dependency-resolver.js'; - -const fixtures = join(dirname(fileURLToPath(import.meta.url)), '../../../fixtures'); - -describe('GithubDependencyResolver', () => { - let resolver: NoirDependencyResolver; - let fm: FileManager; - let pkg: NoirPackage; - let libDependency: NoirGitDependencyConfig; - let fetchMock: jest.SpiedFunction; - - beforeEach(() => { - fm = createMemFSFileManager(createFsFromVolume(new Volume()), '/'); - - libDependency = { - git: 'https://github.com/example/repo', - tag: 'v1.0.0', - }; - - pkg = new NoirPackage('/test_contract', '/test_contract/src', { - dependencies: { - // eslint-disable-next-line camelcase - test_lib: libDependency, - }, - package: { - name: 'test_contract', - type: 'contract', - }, - }); - - resolver = new GithubDependencyResolver(fm); - - // cut off outside access - fetchMock = jest.spyOn(globalThis, 'fetch').mockImplementation(() => { - throw new Error(); - }); - }); - - afterEach(() => { - fetchMock.mockRestore(); - }); - - it("returns null if it can't resolve a dependency", async () => { - const dep = await resolver.resolveDependency(pkg, { - path: '/test_lib', - }); - - expect(dep).toBeNull(); - }); - - it('resolves Github dependency', async () => { - fetchMock.mockResolvedValueOnce(new Response(await readFile(join(fixtures, 'test_lib.zip')), { status: 200 })); - const lib = await resolver.resolveDependency(pkg, libDependency); - expect(lib).toBeDefined(); - expect(lib!.version).toEqual(libDependency.tag); - expect(fm.hasFileSync(lib!.package.getEntryPointPath())).toBe(true); - }); - - it.each<[NoirGitDependencyConfig, 'zip' | 'tar', string]>([ - [ - { - git: 'https://github.com/example/lib.nr', - tag: 'v1.0.0', - }, - 'zip', - 'https://github.com/example/lib.nr/archive/v1.0.0.zip', - ], - [ - { - git: 'https://github.com/example/lib.nr', - tag: 'v1.0.0', - }, - 'tar', - 'https://github.com/example/lib.nr/archive/v1.0.0.tar.gz', - ], - [ - { - git: 'https://github.com/example/lib.nr', - tag: 'HEAD', - }, - 'zip', - 'https://github.com/example/lib.nr/archive/HEAD.zip', - ], - [ - { - git: 'https://github.com/example/lib.nr', - tag: 'HEAD', - }, - 'tar', - 'https://github.com/example/lib.nr/archive/HEAD.tar.gz', - ], - ])('resolves to the correct code archive URL', (dep, format, href) => { - const archiveUrl = resolveGithubCodeArchive(dep, format); - expect(archiveUrl.href).toEqual(href); - }); - - it.each([ - { git: 'https://github.com/', tag: 'v1' }, - { git: 'https://github.com/foo', tag: 'v1' }, - { git: 'https://example.com', tag: 'v1' }, - ])('throws if the Github URL is invalid', dep => { - expect(() => resolveGithubCodeArchive(dep, 'zip')).toThrow(); - }); - - it.each([ - ['main', 'main'], - ['v1.0.0', 'v1.0.0'], - ['../../../etc/passwd', '.._.._.._etc_passwd'], - ['/etc/passwd', 'etc_passwd'], - ['/SomeOrg/some-repo@v1.0.0', 'SomeOrg_some-repo@v1.0.0'], - ['SomeOrg/some-repo@v1.0.0', 'SomeOrg_some-repo@v1.0.0'], - ])('generates safe file names', (value, expected) => { - expect(safeFilename(value)).toEqual(expected); - }); - - it.each([''])('rejects invalid values', value => { - expect(() => safeFilename(value)).toThrow(); - }); -}); diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/github-dependency-resolver.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/github-dependency-resolver.ts deleted file mode 100644 index f7115b562588..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/github-dependency-resolver.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { createDebugOnlyLogger } from '@aztec/foundation/log'; -import { NoirDependencyConfig, NoirGitDependencyConfig } from '@aztec/foundation/noir'; - -import { delimiter, join, sep } from 'node:path'; -import { unzip } from 'unzipit'; - -import { FileManager } from '../file-manager/file-manager.js'; -import { NoirPackage } from '../package.js'; -import { NoirDependency, NoirDependencyResolver } from './dependency-resolver.js'; - -/** - * Downloads dependencies from github - */ -export class GithubDependencyResolver implements NoirDependencyResolver { - #fm: FileManager; - #log = createDebugOnlyLogger('aztec:compile:github-dependency-resolver'); - - constructor(fm: FileManager) { - this.#fm = fm; - } - - /** - * Resolves a dependency from github. Returns null if URL is for a different website. - * @param _pkg - The package to resolve the dependency for - * @param dependency - The dependency configuration - * @returns asd - */ - async resolveDependency(_pkg: NoirPackage, dependency: NoirDependencyConfig): Promise { - // TODO accept ssh urls? - // TODO github authentication? - if (!('git' in dependency) || !dependency.git.startsWith('https://github.com')) { - return null; - } - - const archivePath = await this.#fetchZipFromGithub(dependency); - const libPath = await this.#extractZip(dependency, archivePath); - return { - version: dependency.tag, - package: await NoirPackage.open(libPath, this.#fm), - }; - } - - async #fetchZipFromGithub(dependency: Pick): Promise { - if (!dependency.git.startsWith('https://github.com')) { - throw new Error('Only github dependencies are supported'); - } - - const url = resolveGithubCodeArchive(dependency, 'zip'); - const localArchivePath = join('archives', safeFilename(url.pathname)); - - // TODO should check signature before accepting any file - if (this.#fm.hasFileSync(localArchivePath)) { - this.#log('using cached archive', { url: url.href, path: localArchivePath }); - return localArchivePath; - } - - const response = await fetch(url, { - method: 'GET', - }); - - if (!response.ok || !response.body) { - throw new Error(`Failed to fetch ${url}: ${response.statusText}`); - } - - const tmpFile = localArchivePath + '.tmp'; - await this.#fm.writeFile(tmpFile, response.body); - await this.#fm.moveFile(tmpFile, localArchivePath); - - return localArchivePath; - } - - async #extractZip(dependency: NoirGitDependencyConfig, archivePath: string): Promise { - const gitUrl = new URL(dependency.git); - // extract the archive to this location - const extractLocation = join('libs', safeFilename(gitUrl.pathname + '@' + (dependency.tag ?? 'HEAD'))); - - // where we expect to find this package after extraction - // it might already exist if the archive got unzipped previously - const packagePath = join(extractLocation, dependency.directory ?? ''); - - if (this.#fm.hasFileSync(packagePath)) { - this.#log(`Using existing package at ${packagePath}`); - return packagePath; - } - - const { entries } = await unzip(await this.#fm.readFile(archivePath)); - - // extract to a temporary directory, then move it to the final location - // TODO empty the temp directory first - const tmpExtractLocation = extractLocation + '.tmp'; - for (const entry of Object.values(entries)) { - if (entry.isDirectory) { - continue; - } - - // remove the first path segment, because it'll be the archive name - const name = stripSegments(entry.name, 1); - const path = join(tmpExtractLocation, name); - await this.#fm.writeFile(path, (await entry.blob()).stream()); - } - - await this.#fm.moveFile(tmpExtractLocation, extractLocation); - - return packagePath; - } -} - -/** - * Strips the first n segments from a path - */ -function stripSegments(path: string, count: number): string { - const segments = path.split(sep).filter(Boolean); - return segments.slice(count).join(sep); -} - -/** - * Returns a safe filename for a value - * @param val - The value to convert - */ -export function safeFilename(val: string): string { - if (!val) { - throw new Error('invalid value'); - } - - return val.replaceAll(sep, '_').replaceAll(delimiter, '_').replace(/^_+/, ''); -} - -/** - * Resolves a dependency's archive URL. - * @param dependency - The dependency configuration - * @returns The URL to the library archive - */ -export function resolveGithubCodeArchive(dependency: NoirGitDependencyConfig, format: 'zip' | 'tar'): URL { - const gitUrl = new URL(dependency.git); - const [owner, repo] = gitUrl.pathname.slice(1).split('/'); - const ref = dependency.tag ?? 'HEAD'; - const extension = format === 'zip' ? 'zip' : 'tar.gz'; - - if (!owner || !repo || gitUrl.hostname !== 'github.com') { - throw new Error('Invalid Github repository URL'); - } - - return new URL(`https://github.com/${owner}/${repo}/archive/${ref}.${extension}`); -} diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/local-dependency-resolver.test.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/local-dependency-resolver.test.ts deleted file mode 100644 index 5538a571038c..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/local-dependency-resolver.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { fileURLToPath } from '@aztec/foundation/url'; - -import { createFsFromVolume } from 'memfs'; -import { Volume } from 'memfs/lib/volume.js'; -import { readFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; - -import { FileManager } from '../file-manager/file-manager.js'; -import { createMemFSFileManager } from '../file-manager/memfs-file-manager.js'; -import { NoirPackage } from '../package.js'; -import { NoirDependencyResolver } from './dependency-resolver.js'; -import { LocalDependencyResolver } from './local-dependency-resolver.js'; - -describe('DependencyResolver', () => { - let resolver: NoirDependencyResolver; - let fm: FileManager; - let pkg: NoirPackage; - - beforeEach(async () => { - const fixtures = join(dirname(fileURLToPath(import.meta.url)), '../../../fixtures'); - const memFS = createFsFromVolume(new Volume()); - memFS.mkdirSync('/test_contract/src', { recursive: true }); - memFS.mkdirSync('/test_lib/src', { recursive: true }); - memFS.writeFileSync('/test_contract/Nargo.toml', await readFile(join(fixtures, 'test_contract/Nargo.toml'))); - memFS.writeFileSync('/test_contract/src/main.nr', await readFile(join(fixtures, 'test_contract/src/main.nr'))); - memFS.writeFileSync('/test_lib/Nargo.toml', await readFile(join(fixtures, 'test_lib/Nargo.toml'))); - memFS.writeFileSync('/test_lib/src/lib.nr', await readFile(join(fixtures, 'test_lib/src/lib.nr'))); - - fm = createMemFSFileManager(memFS, '/'); - - pkg = await NoirPackage.open('/test_contract', fm); - resolver = new LocalDependencyResolver(fm); - }); - - it("returns null if it can't resolve a dependency", async () => { - const dep = await resolver.resolveDependency(pkg, { - git: 'git@some-git-host', - directory: '/', - tag: 'v1.0.0', - }); - - expect(dep).toBeNull(); - }); - - it.each(['../test_contract', '/test_contract'])('resolves a known dependency', async path => { - const lib = await resolver.resolveDependency(pkg, { - path, - }); - expect(lib).toBeDefined(); - expect(lib!.version).toBeUndefined(); - expect(fm.hasFileSync(lib!.package.getEntryPointPath())).toBe(true); - }); -}); diff --git a/yarn-project/noir-compiler/src/compile/noir/dependencies/local-dependency-resolver.ts b/yarn-project/noir-compiler/src/compile/noir/dependencies/local-dependency-resolver.ts deleted file mode 100644 index ce86063442a4..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/dependencies/local-dependency-resolver.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NoirDependencyConfig } from '@aztec/foundation/noir'; - -import { isAbsolute, join } from 'path'; - -import { FileManager } from '../file-manager/file-manager.js'; -import { NoirPackage } from '../package.js'; -import { NoirDependency, NoirDependencyResolver } from './dependency-resolver.js'; - -/** - * Resolves dependencies on-disk, relative to current package - */ -export class LocalDependencyResolver implements NoirDependencyResolver { - #fm: FileManager; - - constructor(fm: FileManager) { - this.#fm = fm; - } - - async resolveDependency(parent: NoirPackage, config: NoirDependencyConfig): Promise { - if ('path' in config) { - const parentPath = parent.getPackagePath(); - const dependencyPath = isAbsolute(config.path) ? config.path : join(parentPath, config.path); - return { - // unknown version, Nargo.toml doesn't have a version field - version: undefined, - package: await NoirPackage.open(dependencyPath, this.#fm), - }; - } else { - return null; - } - } -} diff --git a/yarn-project/noir-compiler/src/compile/noir/file-manager/file-manager.test.ts b/yarn-project/noir-compiler/src/compile/noir/file-manager/file-manager.test.ts deleted file mode 100644 index cf039e85dca9..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/file-manager/file-manager.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Volume, createFsFromVolume } from 'memfs'; -import { existsSync, mkdtempSync, rmSync } from 'node:fs'; -import * as fs from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { FileManager, FileSystem } from './file-manager.js'; -import { createMemFSFileManager } from './memfs-file-manager.js'; - -const memFS = (): { fm: FileManager; teardown: () => void } => { - const fm = createMemFSFileManager(createFsFromVolume(new Volume()), '/'); - return { - fm, - // no-op, it's all in memory - teardown: () => {}, - }; -}; - -const nodeFM = (): { fm: FileManager; teardown: () => void } => { - const fileSystem: FileSystem = { - existsSync: existsSync, - mkdir: async (dir: string, opts?: { recursive: boolean }) => { - await fs.mkdir(dir, opts); - }, - writeFile: fs.writeFile, - readFile: fs.readFile, - rename: fs.rename, - readdir: fs.readdir, - }; - - const dir = mkdtempSync(join(tmpdir(), 'noir-compiler-test')); - const fm = new FileManager(fileSystem, dir); - - return { - fm, - teardown: () => { - rmSync(dir, { - recursive: true, - }); - }, - }; -}; - -/** - * Declare the default test suite for a file manager - * @param setup - Function to setup a file manager - * @param teardown - Optional function to call at the end of the test - */ -describe.each([memFS, nodeFM])('FileManager', setup => { - let fm: FileManager; - let testFileContent: string; - let testFileBytes: Uint8Array; - let teardown: () => void; - - beforeEach(() => { - ({ fm, teardown } = setup()); - testFileContent = 'foo'; - testFileBytes = new TextEncoder().encode(testFileContent); - }); - - afterEach(() => { - return teardown?.(); - }); - - it('saves files and correctly reads bytes back', async () => { - await fm.writeFile('test.txt', new Blob([testFileBytes]).stream()); - await expect(fm.readFile('test.txt')).resolves.toEqual(testFileBytes); - }); - - it('saves files and correctly reads UTF-8 string back', async () => { - await fm.writeFile('test.txt', new Blob([testFileBytes]).stream()); - await expect(fm.readFile('test.txt', 'utf-8')).resolves.toEqual(testFileContent); - }); - - it('correctly checks if file exists or not', async () => { - expect(fm.hasFileSync('test.txt')).toBe(false); - await fm.writeFile('test.txt', new Blob([testFileBytes]).stream()); - expect(fm.hasFileSync('test.txt')).toBe(true); - }); - - it('moves files', async () => { - await fm.writeFile('test.txt.tmp', new Blob([testFileBytes]).stream()); - expect(fm.hasFileSync('test.txt.tmp')).toBe(true); - - await fm.moveFile('test.txt.tmp', 'test.txt'); - - expect(fm.hasFileSync('test.txt.tmp')).toBe(false); - expect(fm.hasFileSync('test.txt')).toBe(true); - }); -}); diff --git a/yarn-project/noir-compiler/src/compile/noir/file-manager/file-manager.ts b/yarn-project/noir-compiler/src/compile/noir/file-manager/file-manager.ts deleted file mode 100644 index 5b502bf01bc7..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/file-manager/file-manager.ts +++ /dev/null @@ -1,156 +0,0 @@ -import path, { dirname, isAbsolute, join } from 'path'; - -/** - * A file system interface that matches the node fs module. - */ -export interface FileSystem { - /** Checks if the file exists */ - existsSync: (path: string) => boolean; - /** Creates a directory structure */ - mkdir: ( - dir: string, - opts?: { - /** Create parent directories as needed */ - recursive: boolean; - }, - ) => Promise; - /** Writes a file */ - writeFile: (path: string, data: Uint8Array) => Promise; - /** Reads a file */ - readFile: (path: string, encoding?: 'utf-8') => Promise; - /** Renames a file */ - rename: (oldPath: string, newPath: string) => Promise; - /** Reads a directory */ - readdir: ( - path: string, - options?: { - /** Traverse child directories recursively */ - recursive: boolean; - }, - ) => Promise; -} - -/** - * A file manager that writes file to a specific directory but reads globally. - */ -export class FileManager { - #fs: FileSystem; - #dataDir: string; - - constructor(fs: FileSystem, dataDir: string) { - this.#fs = fs; - this.#dataDir = dataDir; - } - - /** - * Saves a file to the data directory. - * @param name - File to save - * @param stream - File contents - */ - public async writeFile(name: string, stream: ReadableStream): Promise { - if (isAbsolute(name)) { - throw new Error("can't write absolute path"); - } - - const path = this.#getPath(name); - const chunks: Uint8Array[] = []; - const reader = stream.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - - chunks.push(value); - } - - const file = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); - let offset = 0; - for (const chunk of chunks) { - file.set(chunk, offset); - offset += chunk.length; - } - - await this.#fs.mkdir(dirname(path), { recursive: true }); - await this.#fs.writeFile(this.#getPath(path), file); - } - - /** - * Reads a file from the filesystem and returns a buffer - * Saves a file to the data directory. - * @param oldName - File to save - * @param newName - File contents - */ - async moveFile(oldName: string, newName: string) { - if (isAbsolute(oldName) || isAbsolute(newName)) { - throw new Error("can't move absolute path"); - } - - const oldPath = this.#getPath(oldName); - const newPath = this.#getPath(newName); - - await this.#fs.mkdir(dirname(newPath), { recursive: true }); - await this.#fs.rename(oldPath, newPath); - } - - /** - * Reads a file from the disk and returns a buffer - * @param name - File to read - */ - public async readFile(name: string): Promise; - /** - * Reads a file from the filesystem as a string - * @param name - File to read - * @param encoding - Encoding to use - */ - public async readFile(name: string, encoding: 'utf-8'): Promise; - /** - * Reads a file from the filesystem - * @param name - File to read - * @param encoding - Encoding to use - */ - public async readFile(name: string, encoding?: 'utf-8'): Promise { - const path = this.#getPath(name); - const data = await this.#fs.readFile(path, encoding); - - if (!encoding) { - return typeof data === 'string' - ? new TextEncoder().encode(data) // this branch shouldn't be hit, but just in case - : new Uint8Array(data.buffer, data.byteOffset, data.byteLength / Uint8Array.BYTES_PER_ELEMENT); - } - - return data; - } - - /** - * Checks if a file exists and is accessible - * @param name - File to check - */ - public hasFileSync(name: string): boolean { - return this.#fs.existsSync(this.#getPath(name)); - } - - #getPath(name: string) { - return isAbsolute(name) ? name : join(this.#dataDir, name); - } - - /** - * Reads a file from the filesystem - * @param dir - File to read - * @param options - Readdir options - */ - public async readdir( - dir: string, - options?: { - /** - * Traverse child directories recursively - */ - recursive: boolean; - }, - ) { - const dirPath = this.#getPath(dir); - const files = await this.#fs.readdir(dirPath, options); - return files.map(file => path.join(dirPath, file)); - } -} diff --git a/yarn-project/noir-compiler/src/compile/noir/file-manager/memfs-file-manager.ts b/yarn-project/noir-compiler/src/compile/noir/file-manager/memfs-file-manager.ts deleted file mode 100644 index cbf46def8808..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/file-manager/memfs-file-manager.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IFs, fs } from 'memfs'; -import { IDirent } from 'memfs/lib/node/types/misc.js'; - -import { FileManager } from './file-manager.js'; - -/** - * Creates a new FileManager instance based on a MemFS instance - * @param memFS - the memfs backing instance - * @param dataDir - where to store files - */ -export function createMemFSFileManager(memFS: IFs = fs, dataDir = '/'): FileManager { - const readdirRecursive = async (dir: string): Promise => { - const contents = await memFS.promises.readdir(dir); - let files: string[] = []; - for (const handle in contents) { - if ((handle as unknown as IDirent).isFile()) { - files.push(handle.toString()); - } else { - files = files.concat(await readdirRecursive(handle.toString())); - } - } - return files; - }; - return new FileManager( - { - existsSync: memFS.existsSync.bind(memFS), - mkdir: async ( - dir: string, - options?: { - /** - * Traverse child directories - */ - recursive: boolean; - }, - ) => { - await memFS.promises.mkdir(dir, options); - }, - writeFile: memFS.promises.writeFile.bind(memFS), - rename: memFS.promises.rename.bind(memFS), - readFile: memFS.promises.readFile.bind(memFS), - readdir: async ( - dir: string, - options?: { - /** - * Traverse child directories - */ - recursive: boolean; - }, - ) => { - if (options?.recursive) { - return readdirRecursive(dir); - } - return (await memFS.promises.readdir(dir)).map(handles => handles.toString()); - }, - }, - dataDir, - ); -} diff --git a/yarn-project/noir-compiler/src/compile/noir/file-manager/nodejs-file-manager.ts b/yarn-project/noir-compiler/src/compile/noir/file-manager/nodejs-file-manager.ts deleted file mode 100644 index d001e0c072e4..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/file-manager/nodejs-file-manager.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { existsSync } from 'node:fs'; -import * as fs from 'node:fs/promises'; - -import { FileManager } from './file-manager.js'; - -/** - * Creates a new FileManager instance based on nodejs fs - * @param dataDir - where to store files - */ -export function createNodejsFileManager(dataDir: string): FileManager { - return new FileManager( - { - ...fs, - ...{ - // ExistsSync is not available in the fs/promises module - existsSync, - // This is added here because the node types are not compatible with the FileSystem type for mkdir - // Typescripts tries to use a different variant of the function that is not the one that has the optional options. - mkdir: async ( - dir: string, - opts?: { - /** - * Traverse child directories - */ - recursive: boolean; - }, - ) => { - await fs.mkdir(dir, opts); - }, - }, - }, - dataDir, - ); -} diff --git a/yarn-project/noir-compiler/src/compile/noir/noir-wasm-compiler.ts b/yarn-project/noir-compiler/src/compile/noir/noir-wasm-compiler.ts deleted file mode 100644 index 8abd9e9471aa..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/noir-wasm-compiler.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { LogFn, createDebugLogger } from '@aztec/foundation/log'; - -import { CompileError, PathToFileSourceMap, compile } from '@noir-lang/noir_wasm'; -import { isAbsolute } from 'node:path'; - -import { NoirCompilationResult, NoirProgramCompilationArtifacts } from '../../noir_artifact.js'; -import { NoirDependencyManager } from './dependencies/dependency-manager.js'; -import { GithubDependencyResolver as GithubCodeArchiveDependencyResolver } from './dependencies/github-dependency-resolver.js'; -import { LocalDependencyResolver } from './dependencies/local-dependency-resolver.js'; -import { FileManager } from './file-manager/file-manager.js'; -import { NoirPackage } from './package.js'; - -/** Compilation options */ -export type NoirWasmCompileOptions = { - /** Logging function */ - log: LogFn; - /** Log debugging information through this function */ - debugLog?: LogFn; -}; - -/** - * Noir Package Compiler - */ -export class NoirWasmContractCompiler { - #log: LogFn; - #debugLog: LogFn; - #package: NoirPackage; - #fm: FileManager; - #dependencyManager: NoirDependencyManager; - - private constructor( - entrypoint: NoirPackage, - dependencyManager: NoirDependencyManager, - fileManager: FileManager, - opts: NoirWasmCompileOptions, - ) { - this.#log = opts.log; - this.#debugLog = opts.debugLog ?? createDebugLogger('aztec:noir-compiler:wasm'); - this.#package = entrypoint; - this.#fm = fileManager; - this.#dependencyManager = dependencyManager; - } - - /** - * Creates a new compiler instance. - * @param fileManager - The file manager to use - * @param projectPath - The path to the project - * @param opts - Compilation options - */ - public static async new(fileManager: FileManager, projectPath: string, opts: NoirWasmCompileOptions) { - if (!isAbsolute(projectPath)) { - throw new Error('projectPath must be an absolute path'); - } - - const noirPackage = await NoirPackage.open(projectPath, fileManager); - - const dependencyManager = new NoirDependencyManager( - [ - new LocalDependencyResolver(fileManager), - new GithubCodeArchiveDependencyResolver(fileManager), - // TODO support actual Git repositories - ], - noirPackage, - ); - - return new NoirWasmContractCompiler(noirPackage, dependencyManager, fileManager, opts); - } - - /** - * Gets the version of Aztec.nr that was used compiling this contract. - */ - public getResolvedAztecNrVersion() { - // TODO eliminate this hardcoded library name! - // see docs/docs/dev_docs/contracts/setup.md - return this.#dependencyManager.getVersionOf('aztec'); - } - - /** - * Compile EntryPoint - */ - public async compile(): Promise { - if (this.#package.getType() === 'contract') { - this.#debugLog(`Compiling Contract at ${this.#package.getEntryPointPath()}`); - return await this.compileContract(); - } else if (this.#package.getType() === 'bin') { - this.#debugLog(`Compiling Program at ${this.#package.getEntryPointPath()}`); - return await this.compileProgram(); - } else { - this.#log( - `Compile skipped - only supports compiling "contract" and "bin" package types (${this.#package.getType()})`, - ); - return []; - } - } - - /** - * Compiles the Program. - */ - public async compileProgram(): Promise { - await this.#dependencyManager.resolveDependencies(); - this.#debugLog(`Dependencies: ${this.#dependencyManager.getPackageNames().join(', ')}`); - - try { - const isContract: boolean = false; - - const entrypoint = this.#package.getEntryPointPath(); - const deps = { - /* eslint-disable camelcase */ - root_dependencies: this.#dependencyManager.getEntrypointDependencies(), - library_dependencies: this.#dependencyManager.getLibraryDependencies(), - /* eslint-enable camelcase */ - }; - const packageSources = await this.#package.getSources(this.#fm); - const librarySources = ( - await Promise.all( - this.#dependencyManager - .getLibraries() - .map(async ([alias, library]) => await library.package.getSources(this.#fm, alias)), - ) - ).flat(); - const sourceMap: PathToFileSourceMap = new PathToFileSourceMap(); - [...packageSources, ...librarySources].forEach(sourceFile => { - sourceMap.add_source_code(sourceFile.path, sourceFile.source); - }); - const result = compile(entrypoint, isContract, deps, sourceMap); - - if (!('program' in result)) { - throw new Error('No program found in compilation result'); - } - - return [{ name: this.#package.getNoirPackageConfig().package.name, ...result }]; - } catch (err) { - if (err instanceof Error && err.name === 'CompileError') { - await this.#processCompileError(err as CompileError); - } - - throw err; - } - } - - /** - * Compiles the Contract. - */ - public async compileContract(): Promise { - if (!(this.#package.getType() === 'contract' || this.#package.getType() === 'bin')) { - this.#log( - `Compile skipped - only supports compiling "contract" and "bin" package types (${this.#package.getType()})`, - ); - return []; - } - this.#debugLog(`Compiling contract at ${this.#package.getEntryPointPath()}`); - await this.#dependencyManager.resolveDependencies(); - this.#debugLog(`Dependencies: ${this.#dependencyManager.getPackageNames().join(', ')}`); - - try { - const isContract: boolean = true; - - const entrypoint = this.#package.getEntryPointPath(); - const deps = { - /* eslint-disable camelcase */ - root_dependencies: this.#dependencyManager.getEntrypointDependencies(), - library_dependencies: this.#dependencyManager.getLibraryDependencies(), - /* eslint-enable camelcase */ - }; - const packageSources = await this.#package.getSources(this.#fm); - const librarySources = ( - await Promise.all( - this.#dependencyManager - .getLibraries() - .map(async ([alias, library]) => await library.package.getSources(this.#fm, alias)), - ) - ).flat(); - const sourceMap: PathToFileSourceMap = new PathToFileSourceMap(); - [...packageSources, ...librarySources].forEach(sourceFile => { - sourceMap.add_source_code(sourceFile.path, sourceFile.source); - }); - const result = compile(entrypoint, isContract, deps, sourceMap); - - if (!('contract' in result)) { - throw new Error('No contract found in compilation result'); - } - - return [result]; - } catch (err) { - if (err instanceof Error && err.name === 'CompileError') { - await this.#processCompileError(err as CompileError); - throw new Error('Compilation failed'); - } - - throw err; - } - } - - async #resolveFile(path: string) { - try { - const libFile = this.#dependencyManager.findFile(path); - return await this.#fm.readFile(libFile ?? path, 'utf-8'); - } catch (err) { - return ''; - } - } - - async #processCompileError(err: CompileError): Promise { - for (const diag of err.diagnostics) { - this.#log(` ${diag.message}`); - const contents = await this.#resolveFile(diag.file); - const lines = contents.split('\n'); - const lineOffsets = lines.reduce((accum, _, idx) => { - if (idx === 0) { - accum.push(0); - } else { - accum.push(accum[idx - 1] + lines[idx - 1].length + 1); - } - return accum; - }, []); - - for (const secondary of diag.secondaries) { - const errorLine = lineOffsets.findIndex(offset => offset > secondary.start); - this.#log(` ${diag.file}:${errorLine}: ${contents.slice(secondary.start, secondary.end)}`); - } - } - } -} diff --git a/yarn-project/noir-compiler/src/compile/noir/package.ts b/yarn-project/noir-compiler/src/compile/noir/package.ts deleted file mode 100644 index f144830acd9b..000000000000 --- a/yarn-project/noir-compiler/src/compile/noir/package.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { NoirDependencyConfig, NoirPackageConfig, parseNoirPackageConfig } from '@aztec/foundation/noir'; - -import { parse } from '@iarna/toml'; -import { join } from 'node:path'; - -import { FileManager } from './file-manager/file-manager.js'; - -const CONFIG_FILE_NAME = 'Nargo.toml'; -const SOURCE_EXTENSIONS = ['.nr']; - -/** - * An array of sources for a package - */ -type SourceList = Array<{ - /** - * The source path, taking into account modules and aliases. Eg: mylib/mod/mysource.nr - */ - path: string; - /** - * Resolved source plaintext - */ - source: string; -}>; - -/** - * A Noir package. - */ -export class NoirPackage { - #packagePath: string; - #srcPath: string; - #config: NoirPackageConfig; - #version: string | null = null; - - public constructor(path: string, srcDir: string, config: NoirPackageConfig) { - this.#packagePath = path; - this.#srcPath = srcDir; - this.#config = config; - } - - /** - * Gets this package's path. - */ - public getPackagePath() { - return this.#packagePath; - } - - /** - * Gets this package's Nargo.toml (NoirPackage)Config. - */ - public getNoirPackageConfig() { - return this.#config; - } - - /** - * The path to the source directory. - */ - public getSrcPath() { - return this.#srcPath; - } - - /** - * Gets the entrypoint path for this package. - */ - public getEntryPointPath(): string { - let entrypoint: string; - - switch (this.getType()) { - case 'lib': - // we shouldn't need to compile `lib` type, since the .nr source is read directly - // when the lib is used as a dependency elsewhere. - entrypoint = 'lib.nr'; - break; - case 'contract': - case 'bin': - entrypoint = 'main.nr'; - break; - default: - throw new Error(`Unknown package type: ${this.getType()}`); - } - // TODO check that `src` exists - return join(this.#srcPath, entrypoint); - } - - /** - * Gets the project type - */ - public getType() { - return this.#config.package.type; - } - - /** - * Gets this package's dependencies. - */ - public getDependencies(): Record { - return this.#config.dependencies; - } - - /** - * Gets this package's sources. - * @param fm - A file manager to use - * @param alias - An alias for the sources, if this package is a dependency - */ - public async getSources(fm: FileManager, alias?: string): Promise { - const handles = await fm.readdir(this.#srcPath, { recursive: true }); - return Promise.all( - handles - .filter(handle => SOURCE_EXTENSIONS.find(ext => handle.endsWith(ext))) - .map(async file => { - const suffix = file.replace(this.#srcPath, ''); - return { - path: this.getType() === 'lib' ? `${alias ? alias : this.#config.package.name}${suffix}` : file, - source: (await fm.readFile(file, 'utf-8')).toString(), - }; - }), - ); - } - - /** - * Opens a path on the filesystem. - * @param path - Path to the package. - * @param fm - A file manager to use. - * @returns The Noir package at the given location - */ - public static async open(path: string, fm: FileManager): Promise { - const fileContents = await fm.readFile(join(path, CONFIG_FILE_NAME), 'utf-8'); - const config = parseNoirPackageConfig(parse(fileContents)); - - return new NoirPackage(path, join(path, 'src'), config); - } -} diff --git a/yarn-project/noir-compiler/src/index.test.ts b/yarn-project/noir-compiler/src/index.test.ts deleted file mode 100644 index 4922c1af04cf..000000000000 --- a/yarn-project/noir-compiler/src/index.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ContractArtifact } from '@aztec/foundation/abi'; -import { LogFn, createDebugLogger } from '@aztec/foundation/log'; -import { fileURLToPath } from '@aztec/foundation/url'; - -import { execSync } from 'child_process'; -import path from 'path'; - -import { - ProgramArtifact, - compileUsingNargo, - compileUsingNoirWasm, - generateNoirContractInterface, - generateTypescriptContractInterface, -} from './index.js'; - -function isNargoAvailable() { - try { - execSync(`which nargo`); - return true; - } catch (error) { - return false; - } -} - -describe('noir-compiler', () => { - let projectPath: string; - let log: LogFn; - beforeAll(() => { - const currentDirName = path.dirname(fileURLToPath(import.meta.url)); - projectPath = path.join(currentDirName, 'fixtures/test_contract'); - log = createDebugLogger('noir-compiler:test'); - }); - - const nargoAvailable = isNargoAvailable(); - const conditionalDescribe = nargoAvailable ? describe : describe.skip; - const conditionalIt = nargoAvailable ? it : it.skip; - const withoutDebug = ({ - debug: _debug, - ...rest - }: ContractArtifact | ProgramArtifact): Omit => rest; - - function compilerTest( - compileFn: (path: string, opts: { log: LogFn }) => Promise<(ProgramArtifact | ContractArtifact)[]>, - ) { - let compiled: (ProgramArtifact | ContractArtifact)[]; - let compiledContract: ContractArtifact[]; - - beforeAll(async () => { - compiled = await compileFn(projectPath, { log }); - compiledContract = compiled.map(_compiled => _compiled as ContractArtifact); - }); - - it('compiles the test contract', () => { - expect(compiledContract.map(withoutDebug)).toMatchSnapshot(); - }); - - it('generates typescript interface', () => { - const result = generateTypescriptContractInterface(compiledContract[0], `../target/test.json`); - expect(result).toMatchSnapshot(); - }); - - it('generates Aztec.nr external interface', () => { - const result = generateNoirContractInterface(compiledContract[0]); - expect(result).toMatchSnapshot(); - }); - } - - describe('using wasm binary', () => { - compilerTest(compileUsingNoirWasm); - }); - - conditionalDescribe('using nargo', () => { - compilerTest(compileUsingNargo); - }); - - conditionalIt('both nargo and noir_wasm should compile identically', async () => { - const [noirWasmArtifact, nargoArtifact] = await Promise.all([ - compileUsingNoirWasm(projectPath, { log }), - compileUsingNargo(projectPath, { log }), - ]); - - expect(nargoArtifact.map(withoutDebug)).toEqual(noirWasmArtifact.map(withoutDebug)); - }); -}); diff --git a/yarn-project/noir-compiler/src/index.ts b/yarn-project/noir-compiler/src/index.ts index 976c7eaf69df..836c2e8b2c9e 100644 --- a/yarn-project/noir-compiler/src/index.ts +++ b/yarn-project/noir-compiler/src/index.ts @@ -1,49 +1,8 @@ -import { ContractArtifact } from '@aztec/foundation/abi'; - -import { join, resolve } from 'path'; - -import { CompileOpts, NargoContractCompiler } from './compile/nargo.js'; -import { createNodejsFileManager } from './compile/noir/file-manager/nodejs-file-manager.js'; -import { NoirWasmCompileOptions, NoirWasmContractCompiler } from './compile/noir/noir-wasm-compiler.js'; -import { generateArtifact, generateContractArtifact } from './contract-interface-gen/abi.js'; -import { ProgramArtifact } from './noir_artifact.js'; - export * from './versions.js'; export { generateTypescriptContractInterface } from './contract-interface-gen/contractTypescript.js'; export { generateNoirContractInterface } from './contract-interface-gen/noir.js'; export { generateTypescriptProgramInterface } from './contract-interface-gen/programTypescript.js'; -export { generateContractArtifact }; +export { generateContractArtifact } from './contract-interface-gen/abi.js'; export * from './noir_artifact.js'; - -/** - * Compile Aztec.nr contracts in project path using a nargo binary available in the shell. - * @param projectPath - Path to project. - * @param opts - Compiler options. - * @returns Compiled artifacts. - */ -export async function compileUsingNargo(projectPath: string, opts: CompileOpts = {}): Promise { - return (await new NargoContractCompiler(projectPath, opts).compile()).map(artifact => - generateContractArtifact(artifact), - ); -} - -/** - * Compile Aztec.nr contracts in project path using built-in noir_wasm. - * @param projectPath - Path to project. - * @param opts - Compiler options. - * @returns Compiled artifacts. - */ -export async function compileUsingNoirWasm( - projectPath: string, - opts: NoirWasmCompileOptions, -): Promise<(ContractArtifact | ProgramArtifact)[]> { - const cacheRoot = process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? '', '.cache'); - const fileManager = createNodejsFileManager(join(cacheRoot, 'aztec-noir-compiler')); - const compiler = await NoirWasmContractCompiler.new(fileManager, resolve(projectPath), opts); - const artifacts = await compiler.compile(); - return artifacts.map(artifact => { - return generateArtifact(artifact); - }); -}