Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
131 changes: 131 additions & 0 deletions crates/fe/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,137 @@ 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 contract = contract.map(str::to_string).or_else(|| {
metadata["settings"]["compilationTarget"]
.as_object()
.and_then(|target| target.values().next())
.and_then(|name| name.as_str())
.map(str::to_string)
Comment on lines +383 to +388

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor compilationTarget source path

When metadata comes from fe build --emit metadata for an ingot that has the same contract name in more than one source file, settings.compilationTarget is what disambiguates the intended source. This code keeps only the object value and passes just the contract name to the ingot build, so the rebuild sees every module with that name and can fail with the existing duplicate-root-object check instead of rebuilding the recorded target. Use the compilationTarget key to select or isolate the recorded source path before building.

Useful? React with 👍 / 👎.

});

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(),
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
25 changes: 25 additions & 0 deletions crates/fe/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod dependency_diagnostics;
mod doc;
#[cfg(feature = "doc-server")]
mod doc_serve;
mod metadata_input;
mod report;
mod test;
#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -111,6 +112,17 @@ pub enum Command {
/// Treat a `.fe` file target as standalone, even if it is inside an ingot.
#[arg(long)]
standalone: bool,
/// Rebuild from a `<Contract>.metadata.json` recompilation input produced by
/// `--emit metadata` (`-` reads the JSON from stdin).
///
/// The recorded project is materialized into a temporary directory and built with
/// the settings captured in the metadata. Artifacts default to `./out`.
#[arg(
long,
value_name = "PATH",
conflicts_with_all = ["path", "ingot", "standalone", "report"]
)]
from_metadata: Option<Utf8PathBuf>,
/// Build a specific contract by name (defaults to all contracts in the target).
#[arg(long)]
contract: Option<String>,
Expand Down Expand Up @@ -456,6 +468,7 @@ pub fn run(opts: &Options) {
path,
ingot,
standalone,
from_metadata,
contract,
optimize,
out_dir,
Expand All @@ -466,6 +479,18 @@ pub fn run(opts: &Options) {
report_failed_only,
recovery_mode,
} => {
if let Some(metadata_path) = from_metadata {
build::build_from_metadata(
metadata_path,
contract.as_deref(),
optimize.as_deref(),
emit,
out_dir.as_ref(),
profile,
*recovery_mode,
);
return;
}
let opt_level = match effective_opt_level(optimize.as_deref()) {
Ok(level) => level,
Err(err) => {
Expand Down
Loading
Loading