Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
30 changes: 30 additions & 0 deletions CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Builds use the Sonatina codegen pipeline and generate EVM bytecode directly.

```
fe build [--standalone] [--contract <name>] [--optimize <level>] [--out-dir <dir>] [--report [--report-out <out>] [--report-failed-only]] [path]
fe build --from-metadata <PATH|-> [--contract <name>] [--optimize <level>] [--out-dir <dir>] [--emit <artifacts>]
```

If `path` is omitted, it defaults to `.`.
Expand Down Expand Up @@ -234,6 +235,35 @@ Filenames are “sanitized” from contract names:

This sanitization is also what the workspace collision check uses.

### Rebuilding from metadata: `--from-metadata`

`fe build --from-metadata <PATH|->` rebuilds a contract from a `<Contract>.metadata.json`
recompilation input (as produced by `--emit metadata`) instead of a source tree. `-` reads the
JSON from stdin, so external toolchains can pipe a synthesized metadata document into a single
`fe build` invocation. All diagnostics go to stderr.

Behavior:

- The recorded project (all `sources`, one regenerated `fe.toml` per `settings.ingots[]` entry) is
materialized into a temporary directory and built through the normal ingot build path; the
bundled `std`/`core` are provided by the compiler.
- **Contract selection**: defaults to the contract recorded in `settings.compilationTarget`;
`--contract <name>` overrides it.
- **Optimizer level**: defaults to `settings.optimizer.level`; an explicit `-O`/`--optimize` wins,
with a warning on stderr that the rebuilt bytecode will not match the verified artifact.
- **Arithmetic**: the per-ingot effective `arithmetic` values from the metadata are applied via
the regenerated `fe.toml` files (`dependency-arithmetic` is deliberately not re-applied; the
metadata records post-forcing values).
- **Version check**: if `compiler.version` differs from the running compiler, a warning is printed
to stderr (no abort); exact bytecode reproduction is only guaranteed with the same version.
- **Output**: artifacts are selected with `--emit` as usual and written to `--out-dir`, which
defaults to `./out` (relative to the current working directory, since there is no project
directory to anchor to).

Errors (exit code 1): unreadable input, invalid JSON, `language != "Fe"`, missing `sources`, or a
missing/rootless `settings.ingots`. Combining `--from-metadata` with the `[path]` argument,
`--ingot`, `--standalone`, or `--report` is a CLI usage error (exit code 2).

### Reports: `--report`

`fe build` can optionally write a `.tar.gz` debugging report (useful for sharing failures):
Expand Down
5 changes: 1 addition & 4 deletions crates/common/src/file/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ impl Workspace {

#[salsa::tracked]
pub fn get_relative_path(self, db: &dyn InputDb, base: Url, file: File) -> Option<Utf8PathBuf> {
let file_url = match self.paths(db).get(&file) {
Some(url) => url.clone(),
None => return None,
};
let file_url = self.get_path(db, file)?;
base.make_relative(&file_url).map(Utf8PathBuf::from)
}

Expand Down
2 changes: 1 addition & 1 deletion crates/fe-web/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ impl DocIndex {
// Look up the trait URL
let trait_url = lookup_trait(&trait_items, &trait_impl.trait_name)
.map(|p| format!("{}/trait", p))
.unwrap_or_else(|| format!("{}/trait", &trait_impl.trait_name));
.unwrap_or_else(|| format!("{}/trait", trait_impl.trait_name));

// Use the target item's path and kind for the type URL
let type_url = format!("{}/{}", item.path, item.kind.as_str());
Expand Down
5 changes: 2 additions & 3 deletions crates/fe/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -881,10 +881,9 @@ fn std_sol_compat_abi_type(
// Match Uint{N} or Int{N} where N is a valid Solidity bit width (8..=256, multiple of 8)
let (prefix, digits) = if let Some(rest) = name.strip_prefix("Uint") {
("uint", rest)
} else if let Some(rest) = name.strip_prefix("Int") {
("int", rest)
} else {
return None;
let rest = name.strip_prefix("Int")?;
("int", rest)
};

let bits: u16 = digits.parse().ok()?;
Expand Down
217 changes: 207 additions & 10 deletions crates/fe/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ pub fn build(
&dir_path,
ingot,
contract,
None,
opt_level,
emit,
out_dir,
Expand Down Expand Up @@ -306,6 +307,144 @@ pub fn build(
}
}

/// `fe build --from-metadata`: rebuild from a `metadata.json` recompilation
/// input instead of a source tree. Reads the metadata (from a file, or stdin
/// for `-`), materializes the recorded project into a temporary directory,
/// and runs the normal ingot build on it. Exits the process on failure.
pub fn build_from_metadata(
input: &Utf8Path,
contract: Option<&str>,
optimize: Option<&str>,
emit: &[BuildEmit],
out_dir: Option<&Utf8PathBuf>,
profile: &str,
use_recovery_mode: bool,
) {
let emit = EmitSelection::from_requested(emit);

let metadata = match crate::metadata_input::read_metadata(input) {
Ok(metadata) => metadata,
Err(err) => {
eprintln!("Error: {err}");
std::process::exit(1);
}
};
if let Err(err) = crate::metadata_input::validate_metadata(&metadata) {
eprintln!("Error: {err}");
std::process::exit(1);
}

// Exact reproduction is only guaranteed with the same compiler version.
if let Some(version) = metadata["compiler"]["version"].as_str() {
let running = env!("CARGO_PKG_VERSION");
if version != running {
Comment thread
cburgdorf marked this conversation as resolved.
eprintln!(
"Warning: metadata was produced by fe {version}, but this is fe {running}; \
the rebuilt bytecode may differ from the original"
);
}
}
// Non-release builds share a package version; the recorded commit pins the
// exact source revision.
if let (Some(recorded), Some(running)) = (
metadata["compiler"]["commit"].as_str(),
option_env!("FE_GIT_HASH").filter(|hash| !hash.is_empty()),
) && recorded != running
{
eprintln!(
"Warning: metadata was produced by fe commit {recorded}, but this is fe commit \
{running}; the rebuilt bytecode may differ from the original"
);
}

// The metadata's optimizer level is the default; an explicit `-O` wins.
let recorded_level = metadata["settings"]["optimizer"]["level"].as_str();
let level = match (optimize, recorded_level) {
(Some(flag), Some(recorded)) if flag != recorded => {
eprintln!(
"Warning: -O {flag} overrides optimizer level {recorded} recorded in the \
metadata; the rebuilt bytecode will not match the verified artifact"
);
flag
}
(Some(flag), _) => flag,
(None, Some(recorded)) => recorded,
(None, None) => "1",
};
let opt_level: OptLevel = match level.parse() {
Ok(level) => level,
Err(err) => {
eprintln!("Error: {err}");
std::process::exit(1);
}
};

// Default target: the contract recorded in `compilationTarget`; an
// explicit `--contract` overrides it.
let recorded_target = metadata["settings"]["compilationTarget"]
.as_object()
.and_then(|target| target.iter().next())
.and_then(|(path, name)| Some((path.clone(), name.as_str()?.to_string())));
let contract = contract
.map(str::to_string)
.or_else(|| recorded_target.as_ref().map(|(_, name)| name.clone()));
// The recorded source path pins which module defines the target, in case
// another module defines a same-named contract; it only applies while
// building the recorded contract.
let target_source = recorded_target
.filter(|(_, name)| contract.as_deref() == Some(name))
.map(|(path, _)| path);

let temp = match tempfile::Builder::new()
.prefix("fe-from-metadata")
.tempdir()
{
Ok(temp) => temp,
Err(err) => {
eprintln!("Error: Failed to create temporary project directory: {err}");
std::process::exit(1);
}
};
let Some(temp_root) = Utf8Path::from_path(temp.path()) else {
eprintln!("Error: temporary project directory path is not valid UTF-8");
std::process::exit(1);
};
let root_dir = match crate::metadata_input::reconstruct_project(&metadata, temp_root) {
Ok(dir) => dir,
Err(err) => {
eprintln!("Error: {err}");
std::process::exit(1);
}
};

let out_dir = out_dir.cloned().unwrap_or_else(|| Utf8PathBuf::from("out"));

let mut db = DriverDataBase::default();
db.compiler_options()
.set_recovery_mode(&mut db)
.to(use_recovery_mode);
db.compilation_settings()
.set_profile(&mut db)
.to(profile.into());

let had_errors = build_directory(
&mut db,
&root_dir,
None,
contract.as_deref(),
target_source.as_deref(),
opt_level,
emit,
Some(&out_dir),
None,
);

drop(temp);
if had_errors {
std::process::exit(1);
}
}

#[allow(clippy::too_many_arguments)]
fn build_file(
db: &mut DriverDataBase,
Expand Down Expand Up @@ -414,6 +553,7 @@ fn build_directory(
dir_path: &Utf8PathBuf,
ingot: Option<&str>,
contract: Option<&str>,
target_source: Option<&str>,
opt_level: OptLevel,
emit: EmitSelection,
out_dir: Option<&Utf8PathBuf>,
Expand Down Expand Up @@ -487,6 +627,7 @@ fn build_directory(
db,
&url,
contract,
target_source,
opt_level,
emit,
&out_dir,
Expand Down Expand Up @@ -602,6 +743,7 @@ fn build_workspace(
db,
&matches[0].url,
Some(contract),
None,
opt_level,
emit,
&out_dir,
Expand Down Expand Up @@ -654,6 +796,7 @@ fn build_workspace(
db,
&member.url,
None,
None,
opt_level,
emit,
&out_dir,
Expand Down Expand Up @@ -853,6 +996,7 @@ fn build_ingot_url(
db: &mut DriverDataBase,
ingot_url: &Url,
contract: Option<&str>,
target_source: Option<&str>,
opt_level: OptLevel,
emit: EmitSelection,
out_dir: &Utf8Path,
Expand Down Expand Up @@ -899,6 +1043,7 @@ fn build_ingot_url(
db,
ingot,
contract,
target_source,
opt_level,
emit,
out_dir,
Expand All @@ -914,6 +1059,7 @@ fn build_ingot(
db: &DriverDataBase,
ingot: hir::Ingot<'_>,
contract: Option<&str>,
target_source: Option<&str>,
opt_level: OptLevel,
emit: EmitSelection,
out_dir: &Utf8Path,
Expand All @@ -930,6 +1076,21 @@ fn build_ingot(
}
};

// `--from-metadata` records the contract's defining source file in
// `settings.compilationTarget`; scope emission to that module so a
// same-named contract in another module cannot make the target ambiguous.
let target_mod = match (target_source, contract) {
(Some(key), Some(name)) => {
let mut sources = BTreeMap::new();
collect_ingot_sources_prefixed(db, ingot, None, &mut sources);
sources
.get(key)
.map(|(top_mod, _)| *top_mod)
.filter(|top_mod| module_defines_contract(db, *top_mod, name))
}
_ => None,
};

if contract_names.is_empty() {
eprintln!("Error: No contracts found to build");
return BuildSummary { had_errors: true };
Expand All @@ -949,8 +1110,13 @@ fn build_ingot(
let mut had_errors = false;
if emit.ir || emit.writes_any_bytecode() {
if emit.ir {
let ir = match codegen::emit_ingot_sonatina_ir_optimized(db, ingot, opt_level, contract)
{
let ir = match target_mod {
Some(top_mod) => {
codegen::emit_module_sonatina_ir_optimized(db, top_mod, opt_level, contract)
}
None => codegen::emit_ingot_sonatina_ir_optimized(db, ingot, opt_level, contract),
};
let ir = match ir {
Ok(ir) => ir,
Err(err) => {
eprintln!("Error: Failed to compile Sonatina IR: {err}");
Expand All @@ -965,14 +1131,19 @@ fn build_ingot(
}
}
if emit.writes_any_bytecode() {
let bytecode =
match codegen::emit_ingot_sonatina_bytecode(db, ingot, opt_level, contract) {
Ok(bytecode) => bytecode,
Err(err) => {
eprintln!("Error: Failed to compile Sonatina bytecode: {err}");
return BuildSummary { had_errors: true };
}
};
let bytecode = match target_mod {
Some(top_mod) => {
codegen::emit_module_sonatina_bytecode(db, top_mod, opt_level, contract)
}
None => codegen::emit_ingot_sonatina_bytecode(db, ingot, opt_level, contract),
};
let bytecode = match bytecode {
Ok(bytecode) => bytecode,
Err(err) => {
eprintln!("Error: Failed to compile Sonatina bytecode: {err}");
return BuildSummary { had_errors: true };
}
};
had_errors |= write_sonatina_bytecode_artifacts(
&names_to_build,
&bytecode,
Expand All @@ -987,6 +1158,9 @@ fn build_ingot(
// For ingot builds, generate ABI from each top-level module
use hir::hir_def::HirIngot;
for top_mod in ingot.all_modules(db) {
if target_mod.is_some_and(|target| *top_mod != target) {
continue;
}
had_errors |= write_abi_artifacts(db, *top_mod, &names_to_build, out_dir, report_dir);
}
}
Expand Down Expand Up @@ -1124,6 +1298,29 @@ fn collect_ingot_contract_names(
Ok(names)
}

/// Whether `top_mod` defines `name` as a contract or manual contract root — the
/// same definitions `write_metadata_artifacts` records in `compilationTarget`.
fn module_defines_contract(db: &DriverDataBase, top_mod: TopLevelMod<'_>, name: &str) -> bool {
if top_mod.all_contracts(db).iter().any(|contract| {
contract
.name(db)
.to_opt()
.is_some_and(|n| n.data(db) == name)
}) {
return true;
}
top_mod.all_funcs(db).iter().any(|&func| {
func.top_mod(db) == top_mod
&& match func.manual_contract_root_attr(db) {
Some(ManualContractRootAttr::Init { contract_name })
| Some(ManualContractRootAttr::Runtime { contract_name }) => {
contract_name.data(db) == name
}
Some(ManualContractRootAttr::Error(_)) | None => false,
}
})
}

fn collect_workspace_contract_names(db: &DriverDataBase, ingot: hir::Ingot<'_>) -> Vec<String> {
let mut names = BTreeSet::new();
for &top_mod in ingot.all_modules(db) {
Expand Down
Loading
Loading