-
Notifications
You must be signed in to change notification settings - Fork 324
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
485 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
yarn-project/noir-compiler/src/compile/noir/dependency-resolver.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { LogFn, createDebugOnlyLogger } from '@aztec/foundation/log'; | ||
|
||
import { join, resolve, sep } from 'node:path'; | ||
import { Readable } from 'node:stream'; | ||
import { finished } from 'node:stream/promises'; | ||
import { ReadableStream } from 'node:stream/web'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { Parse } from 'tar'; | ||
|
||
import { Filemanager } from './filemanager.js'; | ||
import { NoirGitDependencyConfig, NoirLocalDependencyConfig } from './package-config.js'; | ||
|
||
/** | ||
* Noir Dependency Resolver | ||
*/ | ||
export class NoirDependencyResolver { | ||
#libs = new Map<string, string>(); | ||
#fm: Filemanager; | ||
#log: LogFn; | ||
|
||
constructor(fm: Filemanager) { | ||
this.#fm = fm; | ||
this.#log = createDebugOnlyLogger('noir:dependency-resolver'); | ||
} | ||
|
||
/** | ||
* Resolves a dependency. | ||
* @param pkgLocation - Location of the package | ||
* @param name - Name of the dependency | ||
* @param dependency - Dependency to resolve | ||
*/ | ||
public async add( | ||
pkgLocation: string, | ||
name: string, | ||
dependency: NoirGitDependencyConfig | NoirLocalDependencyConfig, | ||
): Promise<void> { | ||
const path = | ||
'git' in dependency ? await this.#fetchRemoteDependency(dependency) : resolve(pkgLocation, dependency.path); | ||
this.#libs.set(name, path); | ||
} | ||
|
||
/** | ||
* Gets the names of the crates in this dependency list | ||
*/ | ||
public getCrateNames() { | ||
return [...this.#libs.keys()]; | ||
} | ||
|
||
/** | ||
* Looks up a dependency | ||
* @param sourceId - The source being resolved | ||
* @returns The path to the resolved file | ||
*/ | ||
public resolve(sourceId: string): string | null { | ||
const [lib, ...path] = sourceId.split('/').filter(x => x); | ||
if (this.#libs.has(lib)) { | ||
return join(this.#libs.get(lib)!, 'src', ...path); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
async #fetchRemoteDependency(dependency: NoirGitDependencyConfig): Promise<string> { | ||
const archivePath = await this.#fetchTarFromGithub(dependency); | ||
const libPath = await this.#extractTar(dependency, archivePath); | ||
return libPath; | ||
} | ||
|
||
async #extractTar(dependency: NoirGitDependencyConfig, archivePath: string): Promise<string> { | ||
const gitUrl = new URL(dependency.git); | ||
const extractLocation = join('libs', gitUrl.pathname.replaceAll('/', '_') + '@' + (dependency.tag ?? 'HEAD')); | ||
const packagePath = join(extractLocation, dependency.directory ?? ''); | ||
|
||
// TODO check contents before reusing old results | ||
if (await this.#fm.hasEntry(packagePath)) { | ||
return packagePath; | ||
} | ||
|
||
const filter = dependency.directory | ||
? (path: string) => { | ||
const pathWithoutArchiveName = stripSegments(path, 1); | ||
return pathWithoutArchiveName.startsWith(dependency.directory!); | ||
} | ||
: () => true; | ||
|
||
const tarParser = new Parse({ | ||
filter: filter, | ||
onentry: async entry => { | ||
// just save files | ||
if (entry.type === 'File') { | ||
this.#log(`inflating file ${entry.path}`); | ||
// there's no `strip: 1` with the parser | ||
await this.#fm.writeFile(join(extractLocation, stripSegments(entry.path, 1)), Readable.from(entry)); | ||
} else { | ||
entry.resume(); | ||
} | ||
}, | ||
}); | ||
|
||
const archive = this.#fm.readFileSync(archivePath, 'binary'); | ||
await finished(Readable.from(archive).pipe(tarParser)); | ||
|
||
return packagePath; | ||
} | ||
|
||
async #fetchTarFromGithub(dependency: Pick<NoirGitDependencyConfig, 'git' | 'tag'>): Promise<string> { | ||
// TODO support actual git hosts | ||
if (!dependency.git.startsWith('https://github.com')) { | ||
throw new Error('Only github dependencies are supported'); | ||
} | ||
|
||
const url = new URL(`${dependency.git}/archive/${dependency.tag ?? 'HEAD'}.tar.gz`); | ||
const localArchivePath = join('archives', url.pathname.replaceAll('/', '_')); | ||
|
||
// TODO should check signature before accepting any file | ||
if (await this.#fm.hasEntry(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}`); | ||
} | ||
|
||
await this.#fm.writeFile(localArchivePath, Readable.fromWeb(response.body as ReadableStream)); | ||
return localArchivePath; | ||
} | ||
} | ||
|
||
/** | ||
* 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); | ||
} |
73 changes: 73 additions & 0 deletions
73
yarn-project/noir-compiler/src/compile/noir/filemanager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { mkdirp } from 'fs-extra'; | ||
import { createWriteStream, readFileSync } from 'node:fs'; | ||
import { access } from 'node:fs/promises'; | ||
import { dirname, isAbsolute, join } from 'node:path'; | ||
import { Readable } from 'node:stream'; | ||
import { finished } from 'node:stream/promises'; | ||
|
||
/** | ||
* A file manager that writes file to a specific directory but reads globally. | ||
*/ | ||
export class Filemanager { | ||
dataDir: string; | ||
|
||
public constructor(dataDir: string) { | ||
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: Readable): Promise<void> { | ||
if (isAbsolute(name)) { | ||
throw new Error("can't check absolute path"); | ||
} | ||
|
||
const path = this.#getPath(name); | ||
await mkdirp(dirname(path)); | ||
await finished(stream.pipe(createWriteStream(path))); | ||
} | ||
|
||
/** | ||
* Reads a file from the disk and returns a buffer | ||
* @param name - File to read | ||
* @param encoding - Binary encoding | ||
*/ | ||
public readFileSync(name: string, encoding: 'binary'): Buffer; | ||
|
||
/** | ||
* Reads a file from the disk and returns a string | ||
* @param name - File to read | ||
* @param encoding - Encoding to use | ||
*/ | ||
public readFileSync(name: string, encoding: 'utf-8'): string; | ||
|
||
/** | ||
* Reads a file from the disk | ||
* @param name - File to read | ||
* @param encoding - Encoding to use | ||
*/ | ||
public readFileSync(name: string, encoding: 'utf-8' | 'binary'): Buffer | string { | ||
return readFileSync(this.#getPath(name), encoding); | ||
} | ||
|
||
/** | ||
* Checks if a file exists and is accessible | ||
* @param name - File to check | ||
*/ | ||
public async hasEntry(name: string): Promise<boolean> { | ||
try { | ||
// TODO check access modes? | ||
await access(this.#getPath(name)); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
|
||
#getPath(name: string) { | ||
return isAbsolute(name) ? name : join(this.dataDir, name); | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
yarn-project/noir-compiler/src/compile/noir/noir-wasm-compiler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { compile } from '@noir-lang/noir_wasm'; | ||
import { mkdirSync, writeFileSync } from 'node:fs'; | ||
import { join } from 'node:path'; | ||
|
||
import { NoirCompilationArtifacts } from '../../noir_artifact.js'; | ||
import { NoirDependencyResolver } from './dependency-resolver.js'; | ||
import { Filemanager } from './filemanager.js'; | ||
import { NoirPackage } from './package.js'; | ||
import { initializeResolver } from './source-resolver.cjs'; | ||
|
||
/** | ||
* Noir Package Compiler | ||
*/ | ||
export class NoirWasmContractCompiler { | ||
#projectPath: string; | ||
public constructor(projectPath: string) { | ||
this.#projectPath = projectPath; | ||
} | ||
|
||
/** | ||
* Compiles the project. | ||
*/ | ||
public async compile(): Promise<NoirCompilationArtifacts[]> { | ||
const noirPackage = await NoirPackage.new(this.#projectPath); | ||
|
||
if (noirPackage.getType() !== 'contract') { | ||
throw new Error('This is not a contract project'); | ||
} | ||
|
||
const cacheRoot = process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? '', '.cache'); | ||
const filemanager = new Filemanager(join(cacheRoot, 'noir_wasm')); | ||
const dependencyResolver = new NoirDependencyResolver(filemanager); | ||
|
||
for (const [name, config] of Object.entries(noirPackage.getDependencies())) { | ||
await dependencyResolver.add(noirPackage.getPackagePath(), name, config); | ||
} | ||
|
||
initializeResolver((sourceId: any) => { | ||
try { | ||
const libFile = dependencyResolver.resolve(sourceId); | ||
return filemanager.readFileSync(libFile ?? sourceId, 'utf-8'); | ||
} catch (err) { | ||
return ''; | ||
} | ||
}); | ||
|
||
/* eslint-disable camelcase */ | ||
const res = await compile({ | ||
entry_point: noirPackage.getEntryPointPath(), | ||
optional_dependencies_set: dependencyResolver.getCrateNames(), | ||
contracts: true, | ||
}); | ||
/* eslint-enable camelcase */ | ||
|
||
mkdirSync(join(this.#projectPath, 'target'), { recursive: true }); | ||
writeFileSync(join(this.#projectPath, 'target', res.name + '.json'), JSON.stringify(res, null, 2)); | ||
|
||
return [ | ||
{ | ||
contract: { | ||
backend: '', | ||
functions: res.functions, | ||
name: res.name, | ||
}, | ||
}, | ||
]; | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
yarn-project/noir-compiler/src/compile/noir/package-config.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* Noir package configuration. | ||
*/ | ||
export type NoirPackageConfig = { | ||
/** Package metadata */ | ||
package: { | ||
/** Package name */ | ||
name: string; | ||
/** Package type */ | ||
type: 'lib' | 'contract' | 'binary'; | ||
}; | ||
/** Package dependencies */ | ||
dependencies: Record<string, NoirGitDependencyConfig | NoirLocalDependencyConfig>; | ||
}; | ||
|
||
/** | ||
* A remote package dependency. | ||
*/ | ||
export type NoirGitDependencyConfig = { | ||
/** Git repository URL. */ | ||
git: string; | ||
/** Tag to check out */ | ||
tag?: string; | ||
/** Where the dependency sits inside the repo */ | ||
directory?: string; | ||
}; | ||
|
||
/** | ||
* A local package dependency. | ||
*/ | ||
export type NoirLocalDependencyConfig = { | ||
/** Path to the dependency */ | ||
path: string; | ||
}; | ||
|
||
/** | ||
* Checks that an object is a package configuration. | ||
* @param config - Config to check | ||
*/ | ||
export function isPackageConfig(config: any): config is NoirPackageConfig { | ||
// TODO: validate | ||
return true; | ||
} |
Oops, something went wrong.