use crate::manifest::GenericManifestFile;
use crate::{
lock::Lock,
manifest::{
build_profile::ExperimentalFlags, Dependency, ManifestFile, MemberManifestFiles,
PackageManifestFile,
},
source::{self, IPFSNode, Source},
BuildProfile,
};
use anyhow::{anyhow, bail, Context, Error, Result};
use byte_unit::{Byte, UnitType};
use forc_tracing::{println_action_green, println_warning};
use forc_util::{
default_output_directory, find_file_name, kebab_to_snake_case, print_compiling,
print_on_failure, print_warnings,
};
use petgraph::{
self, dot,
visit::{Bfs, Dfs, EdgeRef, Walker},
Directed, Direction,
};
use serde::{Deserialize, Serialize};
use std::{
collections::{hash_map, BTreeSet, HashMap, HashSet},
fmt,
fs::{self, File},
hash::{Hash, Hasher},
io::Write,
path::{Path, PathBuf},
str::FromStr,
sync::{atomic::AtomicBool, Arc},
};
pub use sway_core::Programs;
use sway_core::{
abi_generation::{
evm_abi,
fuel_abi::{self, AbiContext},
},
asm_generation::ProgramABI,
decl_engine::DeclRefFunction,
fuel_prelude::{
fuel_crypto,
fuel_tx::{self, Contract, ContractId, StorageSlot},
},
language::{parsed::TreeType, Visibility},
semantic_analysis::namespace,
source_map::SourceMap,
transform::AttributeKind,
write_dwarf, BuildTarget, Engines, FinalizedEntry, LspConfig,
};
use sway_core::{PrintAsm, PrintIr};
use sway_error::{error::CompileError, handler::Handler, warning::CompileWarning};
use sway_types::constants::{CORE, PRELUDE, STD};
use sway_types::{Ident, Span, Spanned};
use sway_utils::{constants, time_expr, PerformanceData, PerformanceMetric};
use tracing::{debug, info};
type GraphIx = u32;
type Node = Pinned;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Edge {
pub name: String,
pub kind: DepKind,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum DepKind {
Library,
Contract { salt: fuel_tx::Salt },
}
pub type Graph = petgraph::stable_graph::StableGraph<Node, Edge, Directed, GraphIx>;
pub type EdgeIx = petgraph::graph::EdgeIndex<GraphIx>;
pub type NodeIx = petgraph::graph::NodeIndex<GraphIx>;
pub type ManifestMap = HashMap<PinnedId, PackageManifestFile>;
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct PinnedId(u64);
#[derive(Debug, Clone)]
pub struct BuiltPackage {
pub descriptor: PackageDescriptor,
pub program_abi: ProgramABI,
pub storage_slots: Vec<StorageSlot>,
pub warnings: Vec<CompileWarning>,
pub source_map: SourceMap,
pub tree_type: TreeType,
pub bytecode: BuiltPackageBytecode,
pub bytecode_without_tests: Option<BuiltPackageBytecode>,
}
#[derive(Debug, Clone)]
pub struct PackageDescriptor {
pub name: String,
pub target: BuildTarget,
pub manifest_file: PackageManifestFile,
pub pinned: Pinned,
}
#[derive(Debug, Clone)]
pub struct BuiltPackageBytecode {
pub bytes: Vec<u8>,
pub entries: Vec<PkgEntry>,
}
#[derive(Debug, Clone)]
pub struct PkgEntry {
pub finalized: FinalizedEntry,
pub kind: PkgEntryKind,
}
#[derive(Debug, Clone)]
pub enum PkgEntryKind {
Main,
Test(PkgTestEntry),
}
#[derive(Debug, Clone)]
pub enum TestPassCondition {
ShouldRevert(Option<u64>),
ShouldNotRevert,
}
#[derive(Debug, Clone)]
pub struct PkgTestEntry {
pub pass_condition: TestPassCondition,
pub span: Span,
pub file_path: Arc<PathBuf>,
}
pub type BuiltWorkspace = Vec<Arc<BuiltPackage>>;
#[derive(Debug, Clone)]
pub enum Built {
Package(Arc<BuiltPackage>),
Workspace(BuiltWorkspace),
}
pub struct CompiledPackage {
pub source_map: SourceMap,
pub tree_type: TreeType,
pub program_abi: ProgramABI,
pub storage_slots: Vec<StorageSlot>,
pub bytecode: BuiltPackageBytecode,
pub root_module: namespace::Module,
pub warnings: Vec<CompileWarning>,
pub metrics: PerformanceData,
}
pub struct CompiledContractDependency {
pub bytecode: Vec<u8>,
pub storage_slots: Vec<StorageSlot>,
}
pub type CompiledContractDeps = HashMap<NodeIx, CompiledContractDependency>;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
pub struct Pkg {
pub name: String,
pub source: Source,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct Pinned {
pub name: String,
pub source: source::Pinned,
}
#[derive(Clone, Debug)]
pub struct BuildPlan {
graph: Graph,
manifest_map: ManifestMap,
compilation_order: Vec<NodeIx>,
}
#[derive(Clone, Debug)]
pub struct PinnedIdParseError;
#[derive(Default, Clone)]
pub struct PkgOpts {
pub path: Option<String>,
pub offline: bool,
pub terse: bool,
pub locked: bool,
pub output_directory: Option<String>,
pub ipfs_node: IPFSNode,
}
#[derive(Default, Clone)]
pub struct PrintOpts {
pub ast: bool,
pub dca_graph: Option<String>,
pub dca_graph_url_format: Option<String>,
pub asm: PrintAsm,
pub bytecode: bool,
pub bytecode_spans: bool,
pub ir: PrintIr,
pub reverse_order: bool,
}
#[derive(Default, Clone)]
pub struct MinifyOpts {
pub json_abi: bool,
pub json_storage_slots: bool,
}
type ContractIdConst = String;
#[derive(Default, Clone)]
pub struct BuildOpts {
pub pkg: PkgOpts,
pub print: PrintOpts,
pub minify: MinifyOpts,
pub binary_outfile: Option<String>,
pub debug_outfile: Option<String>,
pub build_target: BuildTarget,
pub build_profile: String,
pub release: bool,
pub time_phases: bool,
pub metrics_outfile: Option<String>,
pub error_on_warnings: bool,
pub tests: bool,
pub member_filter: MemberFilter,
pub experimental: ExperimentalFlags,
}
#[derive(Clone)]
pub struct MemberFilter {
pub build_contracts: bool,
pub build_scripts: bool,
pub build_predicates: bool,
pub build_libraries: bool,
}
impl Default for MemberFilter {
fn default() -> Self {
Self {
build_contracts: true,
build_scripts: true,
build_predicates: true,
build_libraries: true,
}
}
}
impl MemberFilter {
pub fn only_scripts() -> Self {
Self {
build_contracts: false,
build_scripts: true,
build_predicates: false,
build_libraries: false,
}
}
pub fn only_contracts() -> Self {
Self {
build_contracts: true,
build_scripts: false,
build_predicates: false,
build_libraries: false,
}
}
pub fn only_predicates() -> Self {
Self {
build_contracts: false,
build_scripts: false,
build_predicates: true,
build_libraries: false,
}
}
pub fn filter_outputs(
&self,
build_plan: &BuildPlan,
outputs: HashSet<NodeIx>,
) -> HashSet<NodeIx> {
let graph = build_plan.graph();
let manifest_map = build_plan.manifest_map();
outputs
.into_iter()
.filter(|&node_ix| {
let pkg = &graph[node_ix];
let pkg_manifest = &manifest_map[&pkg.id()];
let program_type = pkg_manifest.program_type();
match program_type {
Ok(program_type) => match program_type {
TreeType::Predicate => self.build_predicates,
TreeType::Script => self.build_scripts,
TreeType::Contract => self.build_contracts,
TreeType::Library { .. } => self.build_libraries,
},
Err(_) => true,
}
})
.collect()
}
}
impl BuildOpts {
pub fn include_tests(self, include_tests: bool) -> Self {
Self {
tests: include_tests,
..self
}
}
}
impl Edge {
pub fn new(name: String, kind: DepKind) -> Edge {
Edge { name, kind }
}
}
impl BuiltPackage {
pub fn write_bytecode(&self, path: &Path) -> Result<()> {
fs::write(path, &self.bytecode.bytes)?;
Ok(())
}
pub fn write_debug_info(&self, out_file: &Path) -> Result<()> {
if matches!(out_file.extension(), Some(ext) if ext == "json") {
let source_map_json =
serde_json::to_vec(&self.source_map).expect("JSON serialization failed");
fs::write(out_file, source_map_json)?;
} else {
let primary_dir = self.descriptor.manifest_file.dir();
let primary_src = self.descriptor.manifest_file.entry_path();
write_dwarf(&self.source_map, primary_dir, &primary_src, out_file)?;
}
Ok(())
}
pub fn json_abi_string(&self, minify_json_abi: bool) -> Result<Option<String>> {
match &self.program_abi {
ProgramABI::Fuel(program_abi) => {
if !program_abi.functions.is_empty() {
let json_string = if minify_json_abi {
serde_json::to_string(&program_abi)
} else {
serde_json::to_string_pretty(&program_abi)
}?;
Ok(Some(json_string))
} else {
Ok(None)
}
}
ProgramABI::Evm(program_abi) => {
if !program_abi.is_empty() {
let json_string = if minify_json_abi {
serde_json::to_string(&program_abi)
} else {
serde_json::to_string_pretty(&program_abi)
}?;
Ok(Some(json_string))
} else {
Ok(None)
}
}
ProgramABI::MidenVM(()) => Ok(None),
}
}
pub fn write_json_abi(&self, path: &Path, minify: &MinifyOpts) -> Result<()> {
if let Some(json_abi_string) = self.json_abi_string(minify.json_abi)? {
let mut file = File::create(path)?;
file.write_all(json_abi_string.as_bytes())?;
}
Ok(())
}
pub fn write_output(
&self,
minify: &MinifyOpts,
pkg_name: &str,
output_dir: &Path,
) -> Result<()> {
if !output_dir.exists() {
fs::create_dir_all(output_dir)?;
}
let bin_path = output_dir.join(pkg_name).with_extension("bin");
self.write_bytecode(&bin_path)?;
let program_abi_stem = format!("{pkg_name}-abi");
let json_abi_path = output_dir.join(program_abi_stem).with_extension("json");
self.write_json_abi(&json_abi_path, minify)?;
debug!(
" Bytecode size: {} bytes ({})",
self.bytecode.bytes.len(),
format_bytecode_size(self.bytecode.bytes.len())
);
match self.tree_type {
TreeType::Contract => {
let storage_slots_stem = format!("{pkg_name}-storage_slots");
let storage_slots_path = output_dir.join(storage_slots_stem).with_extension("json");
let storage_slots_file = File::create(storage_slots_path)?;
let res = if minify.json_storage_slots {
serde_json::to_writer(&storage_slots_file, &self.storage_slots)
} else {
serde_json::to_writer_pretty(&storage_slots_file, &self.storage_slots)
};
res?;
}
TreeType::Predicate => {
let root = format!(
"0x{}",
fuel_tx::Input::predicate_owner(&self.bytecode.bytes)
);
let root_file_name = format!("{}{}", &pkg_name, SWAY_BIN_ROOT_SUFFIX);
let root_path = output_dir.join(root_file_name);
fs::write(root_path, &root)?;
info!(" Predicate root: {}", root);
}
TreeType::Script => {
let bytecode_hash =
format!("0x{}", fuel_crypto::Hasher::hash(&self.bytecode.bytes));
let hash_file_name = format!("{}{}", &pkg_name, SWAY_BIN_HASH_SUFFIX);
let hash_path = output_dir.join(hash_file_name);
fs::write(hash_path, &bytecode_hash)?;
debug!(" Bytecode hash: {}", bytecode_hash);
}
_ => (),
}
Ok(())
}
}
impl Built {
pub fn into_members<'a>(
&'a self,
) -> Box<dyn Iterator<Item = (&Pinned, Arc<BuiltPackage>)> + 'a> {
match self {
Built::Package(pkg) => {
let pinned = &pkg.as_ref().descriptor.pinned;
let pkg = pkg.clone();
Box::new(std::iter::once((pinned, pkg)))
}
Built::Workspace(workspace) => Box::new(
workspace
.iter()
.map(|pkg| (&pkg.descriptor.pinned, pkg.clone())),
),
}
}
pub fn expect_pkg(self) -> Result<Arc<BuiltPackage>> {
match self {
Built::Package(built_pkg) => Ok(built_pkg),
Built::Workspace(_) => bail!("expected `Built` to be `Built::Package`"),
}
}
}
impl BuildPlan {
pub fn from_pkg_opts(pkg_options: &PkgOpts) -> Result<Self> {
let path = &pkg_options.path;
let manifest_dir = if let Some(ref path) = path {
PathBuf::from(path)
} else {
std::env::current_dir()?
};
let manifest_file = ManifestFile::from_dir(manifest_dir)?;
let member_manifests = manifest_file.member_manifests()?;
if member_manifests.is_empty() {
bail!("No member found to build")
}
let lock_path = manifest_file.lock_path()?;
Self::from_lock_and_manifests(
&lock_path,
&member_manifests,
pkg_options.locked,
pkg_options.offline,
&pkg_options.ipfs_node,
)
}
pub fn from_manifests(
manifests: &MemberManifestFiles,
offline: bool,
ipfs_node: &IPFSNode,
) -> Result<Self> {
validate_version(manifests)?;
let mut graph = Graph::default();
let mut manifest_map = ManifestMap::default();
fetch_graph(manifests, offline, ipfs_node, &mut graph, &mut manifest_map)?;
validate_graph(&graph, manifests)?;
let compilation_order = compilation_order(&graph)?;
Ok(Self {
graph,
manifest_map,
compilation_order,
})
}
pub fn from_lock_and_manifests(
lock_path: &Path,
manifests: &MemberManifestFiles,
locked: bool,
offline: bool,
ipfs_node: &IPFSNode,
) -> Result<Self> {
validate_version(manifests)?;
let mut new_lock_cause = None;
let lock = Lock::from_path(lock_path).unwrap_or_else(|e| {
new_lock_cause = if e.to_string().contains("No such file or directory") {
Some(anyhow!("lock file did not exist"))
} else {
Some(e)
};
Lock::default()
});
let mut graph = lock.to_graph().unwrap_or_else(|e| {
new_lock_cause = Some(anyhow!("Invalid lock: {}", e));
Graph::default()
});
let invalid_deps = validate_graph(&graph, manifests)?;
let members: HashSet<String> = manifests
.iter()
.map(|(member_name, _)| member_name.clone())
.collect();
remove_deps(&mut graph, &members, &invalid_deps);
let mut manifest_map = graph_to_manifest_map(manifests, &graph)?;
let _added = fetch_graph(manifests, offline, ipfs_node, &mut graph, &mut manifest_map)?;
let compilation_order = compilation_order(&graph)?;
let plan = Self {
graph,
manifest_map,
compilation_order,
};
let new_lock = Lock::from_graph(plan.graph());
let lock_diff = new_lock.diff(&lock);
if !lock_diff.removed.is_empty() || !lock_diff.added.is_empty() {
new_lock_cause.get_or_insert(anyhow!("lock file did not match manifest"));
}
if let Some(cause) = new_lock_cause {
if locked {
bail!(
"The lock file {} needs to be updated (Cause: {}) \
but --locked was passed to prevent this.",
lock_path.to_string_lossy(),
cause,
);
}
println_action_green(
"Creating",
&format!("a new `Forc.lock` file. (Cause: {})", cause),
);
let member_names = manifests
.iter()
.map(|(_, manifest)| manifest.project.name.to_string())
.collect();
crate::lock::print_diff(&member_names, &lock_diff);
let string = toml::ser::to_string_pretty(&new_lock)
.map_err(|e| anyhow!("failed to serialize lock file: {}", e))?;
fs::write(lock_path, string)
.map_err(|e| anyhow!("failed to write lock file: {}", e))?;
debug!(" Created new lock file at {}", lock_path.display());
}
Ok(plan)
}
pub fn contract_dependencies(&self, node: NodeIx) -> impl Iterator<Item = NodeIx> + '_ {
let graph = self.graph();
let connected: HashSet<_> = Dfs::new(graph, node).iter(graph).collect();
self.compilation_order()
.iter()
.cloned()
.filter(move |&n| n != node)
.filter(|&n| {
graph
.edges_directed(n, Direction::Incoming)
.any(|edge| matches!(edge.weight().kind, DepKind::Contract { .. }))
})
.filter(move |&n| connected.contains(&n))
}
pub fn member_nodes(&self) -> impl Iterator<Item = NodeIx> + '_ {
self.compilation_order()
.iter()
.copied()
.filter(|&n| self.graph[n].source == source::Pinned::MEMBER)
}
pub fn member_pinned_pkgs(&self) -> impl Iterator<Item = Pinned> + '_ {
let graph = self.graph();
self.member_nodes().map(|node| &graph[node]).cloned()
}
pub fn graph(&self) -> &Graph {
&self.graph
}
pub fn manifest_map(&self) -> &ManifestMap {
&self.manifest_map
}
pub fn compilation_order(&self) -> &[NodeIx] {
&self.compilation_order
}
pub fn find_member_index(&self, member_name: &str) -> Option<NodeIx> {
self.member_nodes()
.find(|node_ix| self.graph[*node_ix].name == member_name)
}
pub fn node_deps(&self, n: NodeIx) -> impl '_ + Iterator<Item = NodeIx> {
let bfs = Bfs::new(&self.graph, n);
bfs.iter(&self.graph)
}
pub fn build_profiles(&self) -> impl '_ + Iterator<Item = (String, BuildProfile)> {
let manifest_map = &self.manifest_map;
let graph = &self.graph;
self.member_nodes().flat_map(|member_node| {
manifest_map[&graph[member_node].id()]
.build_profiles()
.map(|(n, p)| (n.clone(), p.clone()))
})
}
pub fn salt(&self, pinned: &Pinned) -> Option<fuel_tx::Salt> {
let graph = self.graph();
let node_ix = graph
.node_indices()
.find(|node_ix| graph[*node_ix] == *pinned);
node_ix.and_then(|node| {
graph
.edges_directed(node, Direction::Incoming)
.map(|e| match e.weight().kind {
DepKind::Library => None,
DepKind::Contract { salt } => Some(salt),
})
.next()
.flatten()
})
}
pub fn visualize(&self, url_file_prefix: Option<String>) -> String {
format!(
"{:?}",
dot::Dot::with_attr_getters(
&self.graph,
&[dot::Config::NodeNoLabel, dot::Config::EdgeNoLabel],
&|_, _| String::new(),
&|_, nr| {
let url = url_file_prefix.clone().map_or(String::new(), |prefix| {
self.manifest_map
.get(&nr.1.id())
.map_or(String::new(), |manifest| {
format!("URL = \"{}{}\"", prefix, manifest.path().to_string_lossy())
})
});
format!("label = \"{}\" shape = box {url}", nr.1.name)
},
)
)
}
}
fn potential_proj_nodes<'a>(g: &'a Graph, proj_name: &'a str) -> impl 'a + Iterator<Item = NodeIx> {
member_nodes(g).filter(move |&n| g[n].name == proj_name)
}
fn find_proj_node(graph: &Graph, proj_name: &str) -> Result<NodeIx> {
let mut potentials = potential_proj_nodes(graph, proj_name);
let proj_node = potentials
.next()
.ok_or_else(|| anyhow!("graph contains no project node"))?;
match potentials.next() {
None => Ok(proj_node),
Some(_) => Err(anyhow!("graph contains more than one project node")),
}
}
fn validate_version(member_manifests: &MemberManifestFiles) -> Result<()> {
for member_pkg_manifest in member_manifests.values() {
validate_pkg_version(member_pkg_manifest)?;
}
Ok(())
}
fn validate_pkg_version(pkg_manifest: &PackageManifestFile) -> Result<()> {
match &pkg_manifest.project.forc_version {
Some(min_forc_version) => {
let crate_version = env!("CARGO_PKG_VERSION");
let toolchain_version = semver::Version::parse(crate_version)?;
if toolchain_version < *min_forc_version {
bail!(
"{:?} requires forc version {} but current forc version is {}\nUpdate the toolchain by following: https://fuellabs.github.io/sway/v{}/introduction/installation.html",
pkg_manifest.project.name,
min_forc_version,
crate_version,
crate_version
);
}
}
None => {}
};
Ok(())
}
fn member_nodes(g: &Graph) -> impl Iterator<Item = NodeIx> + '_ {
g.node_indices()
.filter(|&n| g[n].source == source::Pinned::MEMBER)
}
fn validate_graph(graph: &Graph, manifests: &MemberManifestFiles) -> Result<BTreeSet<EdgeIx>> {
let mut member_pkgs: HashMap<&String, &PackageManifestFile> = manifests.iter().collect();
let member_nodes: Vec<_> = member_nodes(graph)
.filter_map(|n| {
member_pkgs
.remove(&graph[n].name.to_string())
.map(|pkg| (n, pkg))
})
.collect();
if member_nodes.is_empty() {
return Ok(graph.edge_indices().collect());
}
let mut visited = HashSet::new();
let edges = member_nodes
.into_iter()
.flat_map(move |(n, _)| validate_deps(graph, n, manifests, &mut visited))
.collect();
Ok(edges)
}
fn validate_deps(
graph: &Graph,
node: NodeIx,
manifests: &MemberManifestFiles,
visited: &mut HashSet<NodeIx>,
) -> BTreeSet<EdgeIx> {
let mut remove = BTreeSet::default();
for edge in graph.edges_directed(node, Direction::Outgoing) {
let dep_name = edge.weight();
let dep_node = edge.target();
match validate_dep(graph, manifests, dep_name, dep_node) {
Err(_) => {
remove.insert(edge.id());
}
Ok(_) => {
if visited.insert(dep_node) {
let rm = validate_deps(graph, dep_node, manifests, visited);
remove.extend(rm);
}
continue;
}
}
}
remove
}
fn validate_dep(
graph: &Graph,
manifests: &MemberManifestFiles,
dep_edge: &Edge,
dep_node: NodeIx,
) -> Result<PackageManifestFile> {
let dep_name = &dep_edge.name;
let node_manifest = manifests
.get(dep_name)
.ok_or_else(|| anyhow!("Couldn't find manifest file for {}", dep_name))?;
let dep_path = dep_path(graph, node_manifest, dep_node, manifests).map_err(|e| {
anyhow!(
"failed to construct path for dependency {:?}: {}",
dep_name,
e
)
})?;
let dep_manifest = PackageManifestFile::from_dir(&dep_path)?;
let dep_entry = node_manifest
.dep(dep_name)
.ok_or_else(|| anyhow!("no entry in parent manifest"))?;
let dep_source =
Source::from_manifest_dep_patched(node_manifest, dep_name, dep_entry, manifests)?;
let dep_pkg = graph[dep_node].unpinned(&dep_path);
if dep_pkg.source != dep_source {
bail!("dependency node's source does not match manifest entry");
}
validate_dep_manifest(&graph[dep_node], &dep_manifest, dep_edge)?;
Ok(dep_manifest)
}
fn validate_dep_manifest(
dep: &Pinned,
dep_manifest: &PackageManifestFile,
dep_edge: &Edge,
) -> Result<()> {
let dep_program_type = dep_manifest.program_type()?;
match (&dep_program_type, &dep_edge.kind) {
(TreeType::Contract, DepKind::Contract { salt: _ })
| (TreeType::Library { .. }, DepKind::Library) => {}
_ => bail!(
"\"{}\" is declared as a {} dependency, but is actually a {}",
dep.name,
dep_edge.kind,
dep_program_type
),
}
if dep.name != dep_manifest.project.name {
bail!(
"dependency name {:?} must match the manifest project name {:?} \
unless `package = {:?}` is specified in the dependency declaration",
dep.name,
dep_manifest.project.name,
dep_manifest.project.name,
);
}
validate_pkg_version(dep_manifest)?;
Ok(())
}
fn dep_path(
graph: &Graph,
node_manifest: &PackageManifestFile,
dep_node: NodeIx,
manifests: &MemberManifestFiles,
) -> Result<PathBuf> {
let dep = &graph[dep_node];
let dep_name = &dep.name;
match dep.source.dep_path(&dep.name)? {
source::DependencyPath::ManifestPath(path) => Ok(path),
source::DependencyPath::Root(path_root) => {
validate_path_root(graph, dep_node, path_root)?;
if let Some(path) = node_manifest.dep_path(dep_name) {
if path.exists() {
return Ok(path);
}
}
for (_, patch_map) in node_manifest.patches() {
if let Some(Dependency::Detailed(details)) = patch_map.get(&dep_name.to_string()) {
if let Some(ref rel_path) = details.path {
if let Ok(path) = node_manifest.dir().join(rel_path).canonicalize() {
if path.exists() {
return Ok(path);
}
}
}
}
}
bail!(
"no dependency or patch with name {:?} in manifest of {:?}",
dep_name,
node_manifest.project.name
)
}
source::DependencyPath::Member => {
manifests
.values()
.find(|manifest| manifest.project.name == *dep_name)
.map(|manifest| manifest.path().to_path_buf())
.ok_or_else(|| anyhow!("cannot find dependency in the workspace"))
}
}
}
fn remove_deps(
graph: &mut Graph,
member_names: &HashSet<String>,
edges_to_remove: &BTreeSet<EdgeIx>,
) {
let member_nodes: HashSet<_> = member_nodes(graph)
.filter(|&n| member_names.contains(&graph[n].name.to_string()))
.collect();
let node_removal_order = if let Ok(nodes) = petgraph::algo::toposort(&*graph, None) {
nodes
} else {
graph.clear();
return;
};
for &edge in edges_to_remove {
graph.remove_edge(edge);
}
let nodes = node_removal_order.into_iter();
for node in nodes {
if !has_parent(graph, node) && !member_nodes.contains(&node) {
graph.remove_node(node);
}
}
}
fn has_parent(graph: &Graph, node: NodeIx) -> bool {
graph
.edges_directed(node, Direction::Incoming)
.next()
.is_some()
}
impl Pinned {
pub fn id(&self) -> PinnedId {
PinnedId::new(&self.name, &self.source)
}
pub fn unpinned(&self, path: &Path) -> Pkg {
let source = self.source.unpinned(path);
let name = self.name.clone();
Pkg { name, source }
}
}
impl PinnedId {
pub fn new(name: &str, source: &source::Pinned) -> Self {
let mut hasher = hash_map::DefaultHasher::default();
name.hash(&mut hasher);
source.hash(&mut hasher);
Self(hasher.finish())
}
}
impl fmt::Display for DepKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DepKind::Library => write!(f, "library"),
DepKind::Contract { .. } => write!(f, "contract"),
}
}
}
impl fmt::Display for PinnedId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:016X}", self.0)
}
}
impl FromStr for PinnedId {
type Err = PinnedIdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(
u64::from_str_radix(s, 16).map_err(|_| PinnedIdParseError)?,
))
}
}
pub fn compilation_order(graph: &Graph) -> Result<Vec<NodeIx>> {
let rev_pkg_graph = petgraph::visit::Reversed(&graph);
petgraph::algo::toposort(rev_pkg_graph, None).map_err(|_| {
let scc = petgraph::algo::kosaraju_scc(&graph);
let mut path = String::new();
scc.iter()
.filter(|path| path.len() > 1)
.for_each(|cyclic_path| {
let starting_node = &graph[*cyclic_path.last().unwrap()];
path.push_str(&starting_node.name.to_string());
path.push_str(" -> ");
for (node_index, node) in cyclic_path.iter().enumerate() {
path.push_str(&graph[*node].name.to_string());
if node_index != cyclic_path.len() - 1 {
path.push_str(" -> ");
}
}
path.push('\n');
});
anyhow!("dependency cycle detected: {}", path)
})
}
fn graph_to_manifest_map(manifests: &MemberManifestFiles, graph: &Graph) -> Result<ManifestMap> {
let mut manifest_map = HashMap::new();
for pkg_manifest in manifests.values() {
let pkg_name = &pkg_manifest.project.name;
manifest_map.extend(pkg_graph_to_manifest_map(manifests, pkg_name, graph)?);
}
Ok(manifest_map)
}
fn pkg_graph_to_manifest_map(
manifests: &MemberManifestFiles,
pkg_name: &str,
graph: &Graph,
) -> Result<ManifestMap> {
let proj_manifest = manifests
.get(pkg_name)
.ok_or_else(|| anyhow!("Cannot find manifest for {}", pkg_name))?;
let mut manifest_map = ManifestMap::new();
let Ok(proj_node) = find_proj_node(graph, &proj_manifest.project.name) else {
return Ok(manifest_map);
};
let proj_id = graph[proj_node].id();
manifest_map.insert(proj_id, proj_manifest.clone());
let mut bfs = Bfs::new(graph, proj_node);
bfs.next(graph);
while let Some(dep_node) = bfs.next(graph) {
let (parent_manifest, dep_name) = graph
.edges_directed(dep_node, Direction::Incoming)
.find_map(|edge| {
let parent_node = edge.source();
let dep_name = &edge.weight().name;
let parent = &graph[parent_node];
let parent_manifest = manifest_map.get(&parent.id())?;
Some((parent_manifest, dep_name))
})
.ok_or_else(|| anyhow!("more than one root package detected in graph"))?;
let dep_path = dep_path(graph, parent_manifest, dep_node, manifests).map_err(|e| {
anyhow!(
"failed to construct path for dependency {:?}: {}",
dep_name,
e
)
})?;
let dep_manifest = PackageManifestFile::from_dir(&dep_path)?;
let dep = &graph[dep_node];
manifest_map.insert(dep.id(), dep_manifest);
}
Ok(manifest_map)
}
fn validate_path_root(graph: &Graph, path_dep: NodeIx, path_root: PinnedId) -> Result<()> {
let path_root_node = find_path_root(graph, path_dep)?;
if graph[path_root_node].id() != path_root {
bail!(
"invalid `path_root` for path dependency package {:?}",
&graph[path_dep].name
)
}
Ok(())
}
fn find_path_root(graph: &Graph, mut node: NodeIx) -> Result<NodeIx> {
loop {
let pkg = &graph[node];
match pkg.source {
source::Pinned::Path(ref src) => {
let parent = graph
.edges_directed(node, Direction::Incoming)
.next()
.map(|edge| edge.source())
.ok_or_else(|| {
anyhow!(
"Failed to find path root: `path` dependency \"{}\" has no parent",
src
)
})?;
node = parent;
}
source::Pinned::Git(_)
| source::Pinned::Ipfs(_)
| source::Pinned::Member(_)
| source::Pinned::Registry(_) => {
return Ok(node);
}
}
}
}
fn fetch_graph(
member_manifests: &MemberManifestFiles,
offline: bool,
ipfs_node: &IPFSNode,
graph: &mut Graph,
manifest_map: &mut ManifestMap,
) -> Result<HashSet<NodeIx>> {
let mut added_nodes = HashSet::default();
for member_pkg_manifest in member_manifests.values() {
added_nodes.extend(&fetch_pkg_graph(
member_pkg_manifest,
offline,
ipfs_node,
graph,
manifest_map,
member_manifests,
)?);
}
validate_contract_deps(graph)?;
Ok(added_nodes)
}
fn fetch_pkg_graph(
proj_manifest: &PackageManifestFile,
offline: bool,
ipfs_node: &IPFSNode,
graph: &mut Graph,
manifest_map: &mut ManifestMap,
member_manifests: &MemberManifestFiles,
) -> Result<HashSet<NodeIx>> {
let proj_node = if let Ok(proj_node) = find_proj_node(graph, &proj_manifest.project.name) {
proj_node
} else {
let name = proj_manifest.project.name.clone();
let source = source::Pinned::MEMBER;
let pkg = Pinned { name, source };
let pkg_id = pkg.id();
manifest_map.insert(pkg_id, proj_manifest.clone());
graph.add_node(pkg)
};
let fetch_ts = std::time::Instant::now();
let fetch_id = source::fetch_id(proj_manifest.dir(), fetch_ts);
let path_root = graph[proj_node].id();
let mut fetched = graph
.node_indices()
.map(|n| {
let pinned = &graph[n];
let manifest = &manifest_map[&pinned.id()];
let pkg = pinned.unpinned(manifest.dir());
(pkg, n)
})
.collect();
let mut visited = HashSet::default();
fetch_deps(
fetch_id,
offline,
ipfs_node,
proj_node,
path_root,
graph,
manifest_map,
&mut fetched,
&mut visited,
member_manifests,
)
}
#[allow(clippy::too_many_arguments)]
fn fetch_deps(
fetch_id: u64,
offline: bool,
ipfs_node: &IPFSNode,
node: NodeIx,
path_root: PinnedId,
graph: &mut Graph,
manifest_map: &mut ManifestMap,
fetched: &mut HashMap<Pkg, NodeIx>,
visited: &mut HashSet<NodeIx>,
member_manifests: &MemberManifestFiles,
) -> Result<HashSet<NodeIx>> {
let mut added = HashSet::default();
let parent_id = graph[node].id();
let package_manifest = &manifest_map[&parent_id];
let deps: Vec<(String, Dependency, DepKind)> = package_manifest
.contract_deps()
.map(|(n, d)| {
(
n.clone(),
d.dependency.clone(),
DepKind::Contract { salt: d.salt.0 },
)
})
.chain(
package_manifest
.deps()
.map(|(n, d)| (n.clone(), d.clone(), DepKind::Library)),
)
.collect();
for (dep_name, dep, dep_kind) in deps {
let name = dep.package().unwrap_or(&dep_name);
let parent_manifest = &manifest_map[&parent_id];
let source =
Source::from_manifest_dep_patched(parent_manifest, name, &dep, member_manifests)
.context(format!("Failed to source dependency: {dep_name}"))?;
let dep_pkg = Pkg {
name: name.to_string(),
source,
};
let dep_node = match fetched.entry(dep_pkg) {
hash_map::Entry::Occupied(entry) => *entry.get(),
hash_map::Entry::Vacant(entry) => {
let pkg = entry.key();
let ctx = source::PinCtx {
fetch_id,
path_root,
name: &pkg.name,
offline,
ipfs_node,
};
let source = pkg.source.pin(ctx, manifest_map)?;
let name = pkg.name.clone();
let dep_pinned = Pinned { name, source };
let dep_node = graph.add_node(dep_pinned);
added.insert(dep_node);
*entry.insert(dep_node)
}
};
let dep_edge = Edge::new(dep_name.to_string(), dep_kind.clone());
graph.update_edge(node, dep_node, dep_edge.clone());
if !visited.insert(dep_node) {
continue;
}
let dep_pinned = &graph[dep_node];
let dep_pkg_id = dep_pinned.id();
validate_dep_manifest(dep_pinned, &manifest_map[&dep_pkg_id], &dep_edge).map_err(|e| {
let parent = &graph[node];
anyhow!(
"dependency of {:?} named {:?} is invalid: {}",
parent.name,
dep_name,
e
)
})?;
let path_root = match dep_pinned.source {
source::Pinned::Member(_)
| source::Pinned::Git(_)
| source::Pinned::Ipfs(_)
| source::Pinned::Registry(_) => dep_pkg_id,
source::Pinned::Path(_) => path_root,
};
added.extend(fetch_deps(
fetch_id,
offline,
ipfs_node,
dep_node,
path_root,
graph,
manifest_map,
fetched,
visited,
member_manifests,
)?);
}
Ok(added)
}
pub fn sway_build_config(
manifest_dir: &Path,
entry_path: &Path,
build_target: BuildTarget,
build_profile: &BuildProfile,
) -> Result<sway_core::BuildConfig> {
let file_name = find_file_name(manifest_dir, entry_path)?;
let build_config = sway_core::BuildConfig::root_from_file_name_and_manifest_path(
file_name.to_path_buf(),
manifest_dir.to_path_buf(),
build_target,
)
.with_print_dca_graph(build_profile.print_dca_graph.clone())
.with_print_dca_graph_url_format(build_profile.print_dca_graph_url_format.clone())
.with_print_asm(build_profile.print_asm)
.with_print_bytecode(
build_profile.print_bytecode,
build_profile.print_bytecode_spans,
)
.with_print_ir(build_profile.print_ir.clone())
.with_include_tests(build_profile.include_tests)
.with_time_phases(build_profile.time_phases)
.with_metrics(build_profile.metrics_outfile.clone())
.with_optimization_level(build_profile.optimization_level)
.with_experimental(sway_core::ExperimentalFlags {
new_encoding: build_profile.experimental.new_encoding,
});
Ok(build_config)
}
pub const CONTRACT_ID_CONSTANT_NAME: &str = "CONTRACT_ID";
pub fn dependency_namespace(
lib_namespace_map: &HashMap<NodeIx, namespace::Module>,
compiled_contract_deps: &CompiledContractDeps,
graph: &Graph,
node: NodeIx,
engines: &Engines,
contract_id_value: Option<ContractIdConst>,
experimental: sway_core::ExperimentalFlags,
) -> Result<namespace::Root, vec1::Vec1<CompileError>> {
let node_idx = &graph[node];
let name = Ident::new_no_span(node_idx.name.clone());
let mut root_module = if let Some(contract_id_value) = contract_id_value {
namespace::default_with_contract_id(
engines,
name.clone(),
Visibility::Public,
contract_id_value,
experimental,
)?
} else {
namespace::Module::new(name, Visibility::Public, None)
};
let mut core_added = false;
for edge in graph.edges_directed(node, Direction::Outgoing) {
let dep_node = edge.target();
let dep_name = kebab_to_snake_case(&edge.weight().name);
let dep_edge = edge.weight();
let mut dep_namespace = match dep_edge.kind {
DepKind::Library => lib_namespace_map
.get(&dep_node)
.cloned()
.expect("no namespace module")
.read(engines, Clone::clone),
DepKind::Contract { salt } => {
let dep_contract_id = compiled_contract_deps
.get(&dep_node)
.map(|dep| contract_id(&dep.bytecode, dep.storage_slots.clone(), &salt))
.unwrap_or_default();
let contract_id_value = format!("0x{dep_contract_id}");
let node_idx = &graph[dep_node];
let name = Ident::new_no_span(node_idx.name.clone());
namespace::default_with_contract_id(
engines,
name.clone(),
Visibility::Private,
contract_id_value,
experimental,
)?
}
};
dep_namespace.is_external = true;
root_module.insert_submodule(dep_name, dep_namespace);
let dep = &graph[dep_node];
if dep.name == CORE {
core_added = true;
}
}
if !core_added {
if let Some(core_node) = find_core_dep(graph, node) {
let core_namespace = &lib_namespace_map[&core_node];
root_module.insert_submodule(CORE.to_string(), core_namespace.clone());
core_added = true;
}
}
let mut root = namespace::Root::from(root_module);
if core_added {
let _ = root.star_import(
&Handler::default(),
engines,
&[CORE, PRELUDE].map(|s| Ident::new_no_span(s.into())),
&[],
Visibility::Private,
);
}
if has_std_dep(graph, node) {
let _ = root.star_import(
&Handler::default(),
engines,
&[STD, PRELUDE].map(|s| Ident::new_no_span(s.into())),
&[],
Visibility::Private,
);
}
Ok(root)
}
fn has_std_dep(graph: &Graph, node: NodeIx) -> bool {
let pkg = &graph[node];
if pkg.name == STD {
return false;
}
graph.edges_directed(node, Direction::Outgoing).any(|edge| {
let dep_node = edge.target();
let dep = &graph[dep_node];
matches!(&dep.name[..], STD)
})
}
fn find_core_dep(graph: &Graph, node: NodeIx) -> Option<NodeIx> {
let pkg = &graph[node];
if pkg.name == CORE {
return None;
}
let mut maybe_std = None;
for edge in graph.edges_directed(node, Direction::Outgoing) {
let dep_node = edge.target();
let dep = &graph[dep_node];
match &dep.name[..] {
CORE => return Some(dep_node),
STD => maybe_std = Some(dep_node),
_ => {}
}
}
if let Some(std) = maybe_std {
return find_core_dep(graph, std);
}
for dep_node in Dfs::new(graph, node).iter(graph) {
let dep = &graph[dep_node];
if dep.name == CORE {
return Some(dep_node);
}
}
None
}
pub fn compile(
pkg: &PackageDescriptor,
profile: &BuildProfile,
engines: &Engines,
namespace: &mut namespace::Root,
source_map: &mut SourceMap,
) -> Result<CompiledPackage> {
let mut metrics = PerformanceData::default();
let entry_path = pkg.manifest_file.entry_path();
let sway_build_config =
sway_build_config(pkg.manifest_file.dir(), &entry_path, pkg.target, profile)?;
let terse_mode = profile.terse;
let reverse_results = profile.reverse_results;
let fail = |handler: Handler| {
let (errors, warnings) = handler.consume();
print_on_failure(
engines.se(),
terse_mode,
&warnings,
&errors,
reverse_results,
);
bail!("Failed to compile {}", pkg.name);
};
let source = pkg.manifest_file.entry_string()?;
let handler = Handler::default();
let ast_res = time_expr!(
"compile to ast",
"compile_to_ast",
sway_core::compile_to_ast(
&handler,
engines,
source,
namespace,
Some(&sway_build_config),
&pkg.name,
None,
),
Some(sway_build_config.clone()),
metrics
);
let programs = match ast_res {
Err(_) => return fail(handler),
Ok(programs) => programs,
};
let typed_program = match programs.typed.as_ref() {
Err(_) => return fail(handler),
Ok(typed_program) => typed_program,
};
if profile.print_ast {
tracing::info!("{:#?}", typed_program);
}
let storage_slots = typed_program.storage_slots.clone();
let tree_type = typed_program.kind.tree_type();
let namespace = typed_program.root.namespace.clone();
if handler.has_errors() {
return fail(handler);
}
let asm_res = time_expr!(
"compile ast to asm",
"compile_ast_to_asm",
sway_core::ast_to_asm(&handler, engines, &programs, &sway_build_config),
Some(sway_build_config.clone()),
metrics
);
const OLD_ENCODING_VERSION: &str = "0";
const NEW_ENCODING_VERSION: &str = "1";
const SPEC_VERSION: &str = "1";
let mut program_abi = match pkg.target {
BuildTarget::Fuel => {
let program_abi_res = time_expr!(
"generate JSON ABI program",
"generate_json_abi",
fuel_abi::generate_program_abi(
&handler,
&mut AbiContext {
program: typed_program,
abi_with_callpaths: true,
type_ids_to_full_type_str: HashMap::<String, String>::new(),
},
engines,
profile
.experimental
.new_encoding
.then(|| NEW_ENCODING_VERSION.into())
.unwrap_or(OLD_ENCODING_VERSION.into()),
SPEC_VERSION.into(),
),
Some(sway_build_config.clone()),
metrics
);
let program_abi = match program_abi_res {
Err(_) => return fail(handler),
Ok(program_abi) => program_abi,
};
ProgramABI::Fuel(program_abi)
}
BuildTarget::EVM => {
let mut ops = match &asm_res {
Ok(ref asm) => match &asm.0.abi {
Some(ProgramABI::Evm(ops)) => ops.clone(),
_ => vec![],
},
_ => vec![],
};
let abi = time_expr!(
"generate JSON ABI program",
"generate_json_abi",
evm_abi::generate_abi_program(typed_program, engines),
Some(sway_build_config.clone()),
metrics
);
ops.extend(abi);
ProgramABI::Evm(ops)
}
BuildTarget::MidenVM => ProgramABI::MidenVM(()),
};
let entries = asm_res
.as_ref()
.map(|asm| asm.0.entries.clone())
.unwrap_or_default();
let entries = entries
.iter()
.map(|finalized_entry| PkgEntry::from_finalized_entry(finalized_entry, engines))
.collect::<anyhow::Result<_>>()?;
let asm = match asm_res {
Err(_) => return fail(handler),
Ok(asm) => asm,
};
let bc_res = time_expr!(
"compile asm to bytecode",
"compile_asm_to_bytecode",
sway_core::asm_to_bytecode(&handler, asm, source_map, engines.se(), &sway_build_config),
Some(sway_build_config.clone()),
metrics
);
let errored = handler.has_errors() || (handler.has_warnings() && profile.error_on_warnings);
let compiled = match bc_res {
Ok(compiled) if !errored => compiled,
_ => return fail(handler),
};
let (_, warnings) = handler.consume();
print_warnings(engines.se(), terse_mode, &pkg.name, &warnings, &tree_type);
if let ProgramABI::Fuel(ref mut program_abi) = program_abi {
if let Some(ref mut configurables) = program_abi.configurables {
configurables.retain(|c| {
compiled
.named_data_section_entries_offsets
.contains_key(&c.name)
});
for (config, offset) in compiled.named_data_section_entries_offsets {
if let Some(idx) = configurables.iter().position(|c| c.name == config) {
configurables[idx].offset = offset;
}
}
}
}
metrics.bytecode_size = compiled.bytecode.len();
let bytecode = BuiltPackageBytecode {
bytes: compiled.bytecode,
entries,
};
let compiled_package = CompiledPackage {
source_map: source_map.clone(),
program_abi,
storage_slots,
tree_type,
bytecode,
root_module: namespace.root_module().clone(),
warnings,
metrics,
};
Ok(compiled_package)
}
impl PkgEntry {
pub fn is_test(&self) -> bool {
self.kind.test().is_some()
}
fn from_finalized_entry(finalized_entry: &FinalizedEntry, engines: &Engines) -> Result<Self> {
let pkg_entry_kind = match &finalized_entry.test_decl_ref {
Some(test_decl_ref) => {
let pkg_test_entry = PkgTestEntry::from_decl(test_decl_ref, engines)?;
PkgEntryKind::Test(pkg_test_entry)
}
None => PkgEntryKind::Main,
};
Ok(Self {
finalized: finalized_entry.clone(),
kind: pkg_entry_kind,
})
}
}
impl PkgEntryKind {
pub fn test(&self) -> Option<&PkgTestEntry> {
match self {
PkgEntryKind::Test(test) => Some(test),
_ => None,
}
}
}
impl PkgTestEntry {
fn from_decl(decl_ref: &DeclRefFunction, engines: &Engines) -> Result<Self> {
let span = decl_ref.span();
let test_function_decl = engines.de().get_function(decl_ref);
const FAILING_TEST_KEYWORD: &str = "should_revert";
let test_args: HashMap<String, Option<String>> = test_function_decl
.attributes
.get(&AttributeKind::Test)
.expect("test declaration is missing test attribute")
.iter()
.flat_map(|attr| attr.args.iter())
.map(|arg| {
(
arg.name.to_string(),
arg.value
.as_ref()
.map(|val| val.span().as_str().to_string()),
)
})
.collect();
let pass_condition = if test_args.is_empty() {
anyhow::Ok(TestPassCondition::ShouldNotRevert)
} else if let Some(args) = test_args.get(FAILING_TEST_KEYWORD) {
let expected_revert_code = args
.as_ref()
.map(|arg| {
let arg_str = arg.replace('"', "");
arg_str.parse::<u64>()
})
.transpose()?;
anyhow::Ok(TestPassCondition::ShouldRevert(expected_revert_code))
} else {
let test_name = &test_function_decl.name;
bail!("Invalid test argument(s) for test: {test_name}.")
}?;
let file_path = Arc::new(
engines.se().get_path(
span.source_id()
.ok_or_else(|| anyhow::anyhow!("Missing span for test function"))?,
),
);
Ok(Self {
pass_condition,
span,
file_path,
})
}
}
pub const SWAY_BIN_HASH_SUFFIX: &str = "-bin-hash";
pub const SWAY_BIN_ROOT_SUFFIX: &str = "-bin-root";
fn build_profile_from_opts(
build_profiles: &HashMap<String, BuildProfile>,
build_options: &BuildOpts,
) -> Result<BuildProfile> {
let BuildOpts {
pkg,
print,
time_phases,
build_profile,
release,
metrics_outfile,
tests,
error_on_warnings,
experimental,
..
} = build_options;
let selected_profile_name = match release {
true => BuildProfile::RELEASE,
false => build_profile,
};
let mut profile = build_profiles
.get(selected_profile_name)
.cloned()
.unwrap_or_else(|| {
println_warning(&format!(
"The provided profile option {selected_profile_name} is not present in the manifest file. \
Using default profile."
));
BuildProfile::default()
});
profile.name = selected_profile_name.into();
profile.print_ast |= print.ast;
if profile.print_dca_graph.is_none() {
profile.print_dca_graph.clone_from(&print.dca_graph);
}
if profile.print_dca_graph_url_format.is_none() {
profile
.print_dca_graph_url_format
.clone_from(&print.dca_graph_url_format);
}
profile.print_ir |= print.ir.clone();
profile.print_asm |= print.asm;
profile.print_bytecode |= print.bytecode;
profile.print_bytecode_spans |= print.bytecode_spans;
profile.terse |= pkg.terse;
profile.time_phases |= time_phases;
if profile.metrics_outfile.is_none() {
profile.metrics_outfile.clone_from(metrics_outfile);
}
profile.include_tests |= tests;
profile.error_on_warnings |= error_on_warnings;
profile.experimental = ExperimentalFlags {
new_encoding: experimental.new_encoding,
};
Ok(profile)
}
fn profile_target_string(profile_name: &str, build_target: &BuildTarget) -> String {
let mut targets = vec![format!("{build_target}")];
match profile_name {
BuildProfile::DEBUG => targets.insert(0, "unoptimized".into()),
BuildProfile::RELEASE => targets.insert(0, "optimized".into()),
_ => {}
};
format!("{profile_name} [{}] target(s)", targets.join(" + "))
}
pub fn format_bytecode_size(bytes_len: usize) -> String {
let size = Byte::from_u64(bytes_len as u64);
let adjusted_byte = size.get_appropriate_unit(UnitType::Decimal);
adjusted_byte.to_string()
}
fn is_contract_dependency(graph: &Graph, node: NodeIx) -> bool {
graph
.edges_directed(node, Direction::Incoming)
.any(|e| matches!(e.weight().kind, DepKind::Contract { .. }))
}
pub fn build_with_options(build_options: &BuildOpts) -> Result<Built> {
let BuildOpts {
minify,
binary_outfile,
debug_outfile,
pkg,
build_target,
member_filter,
experimental,
..
} = &build_options;
let current_dir = std::env::current_dir()?;
let path = &build_options
.pkg
.path
.as_ref()
.map_or_else(|| current_dir, PathBuf::from);
println_action_green("Building", &path.display().to_string());
let build_plan = BuildPlan::from_pkg_opts(&build_options.pkg)?;
let graph = build_plan.graph();
let manifest_map = build_plan.manifest_map();
let curr_manifest = manifest_map
.values()
.find(|&pkg_manifest| pkg_manifest.dir() == path);
let build_profiles: HashMap<String, BuildProfile> = build_plan.build_profiles().collect();
let build_profile = build_profile_from_opts(&build_profiles, build_options)?;
let outputs = match curr_manifest {
Some(pkg_manifest) => std::iter::once(
build_plan
.find_member_index(&pkg_manifest.project.name)
.ok_or_else(|| anyhow!("Cannot found project node in the graph"))?,
)
.collect(),
None => build_plan.member_nodes().collect(),
};
let outputs = member_filter.filter_outputs(&build_plan, outputs);
let mut built_workspace = Vec::new();
let build_start = std::time::Instant::now();
let built_packages = build(
&build_plan,
*build_target,
&build_profile,
&outputs,
sway_core::ExperimentalFlags {
new_encoding: experimental.new_encoding,
},
)?;
let output_dir = pkg.output_directory.as_ref().map(PathBuf::from);
let total_size = built_packages
.iter()
.map(|(_, pkg)| pkg.bytecode.bytes.len())
.sum::<usize>();
println_action_green(
"Finished",
&format!(
"{} [{}] in {:.2}s",
profile_target_string(&build_profile.name, build_target),
format_bytecode_size(total_size),
build_start.elapsed().as_secs_f32()
),
);
for (node_ix, built_package) in built_packages {
print_pkg_summary_header(&built_package);
let pinned = &graph[node_ix];
let pkg_manifest = manifest_map
.get(&pinned.id())
.ok_or_else(|| anyhow!("Couldn't find member manifest for {}", pinned.name))?;
let output_dir = output_dir.clone().unwrap_or_else(|| {
default_output_directory(pkg_manifest.dir()).join(&build_profile.name)
});
if let Some(outfile) = &binary_outfile {
built_package.write_bytecode(outfile.as_ref())?;
}
if let Some(outfile) = &debug_outfile {
built_package.write_debug_info(outfile.as_ref())?;
}
built_package.write_output(minify, &pkg_manifest.project.name, &output_dir)?;
built_workspace.push(Arc::new(built_package));
}
match curr_manifest {
Some(pkg_manifest) => {
let built_pkg = built_workspace
.into_iter()
.find(|pkg| pkg.descriptor.manifest_file == *pkg_manifest)
.expect("package didn't exist in workspace");
Ok(Built::Package(built_pkg))
}
None => Ok(Built::Workspace(built_workspace)),
}
}
fn print_pkg_summary_header(built_pkg: &BuiltPackage) {
let prog_ty_str = forc_util::program_type_str(&built_pkg.tree_type);
let padded_ty_str = format!("{prog_ty_str:>10}");
let padding = &padded_ty_str[..padded_ty_str.len() - prog_ty_str.len()];
let ty_ansi = ansi_term::Colour::Green.bold().paint(prog_ty_str);
let name_ansi = ansi_term::Style::new()
.bold()
.paint(&built_pkg.descriptor.name);
debug!("{padding}{ty_ansi} {name_ansi}");
}
pub fn contract_id(
bytecode: &[u8],
mut storage_slots: Vec<StorageSlot>,
salt: &fuel_tx::Salt,
) -> ContractId {
let contract = Contract::from(bytecode);
storage_slots.sort();
let state_root = Contract::initial_state_root(storage_slots.iter());
contract.id(salt, &contract.root(), &state_root)
}
fn validate_contract_deps(graph: &Graph) -> Result<()> {
for node in graph.node_indices() {
let pkg = &graph[node];
let name = pkg.name.clone();
let salt_declarations: HashSet<fuel_tx::Salt> = graph
.edges_directed(node, Direction::Incoming)
.filter_map(|e| match e.weight().kind {
DepKind::Library => None,
DepKind::Contract { salt } => Some(salt),
})
.collect();
if salt_declarations.len() > 1 {
bail!(
"There are conflicting salt declarations for contract dependency named: {}\nDeclared salts: {:?}",
name,
salt_declarations,
)
}
}
Ok(())
}
pub fn build(
plan: &BuildPlan,
target: BuildTarget,
profile: &BuildProfile,
outputs: &HashSet<NodeIx>,
experimental: sway_core::ExperimentalFlags,
) -> anyhow::Result<Vec<(NodeIx, BuiltPackage)>> {
let mut built_packages = Vec::new();
let required: HashSet<NodeIx> = outputs
.iter()
.flat_map(|output_node| plan.node_deps(*output_node))
.collect();
let engines = Engines::default();
let include_tests = profile.include_tests;
let mut contract_id_value: Option<ContractIdConst> = None;
let mut lib_namespace_map = HashMap::default();
let mut compiled_contract_deps = HashMap::new();
for &node in plan
.compilation_order
.iter()
.filter(|node| required.contains(node))
{
let mut source_map = SourceMap::new();
let pkg = &plan.graph()[node];
let manifest = &plan.manifest_map()[&pkg.id()];
let program_ty = manifest.program_type().ok();
print_compiling(
program_ty.as_ref(),
&pkg.name,
&pkg.source.display_compiling(manifest.dir()),
);
let descriptor = PackageDescriptor {
name: pkg.name.clone(),
target,
pinned: pkg.clone(),
manifest_file: manifest.clone(),
};
let fail = |warnings, errors| {
print_on_failure(
engines.se(),
profile.terse,
warnings,
errors,
profile.reverse_results,
);
bail!("Failed to compile {}", pkg.name);
};
let is_contract_dependency = is_contract_dependency(plan.graph(), node);
let bytecode_without_tests = if (include_tests
&& matches!(manifest.program_type(), Ok(TreeType::Contract)))
|| is_contract_dependency
{
let profile = BuildProfile {
include_tests: false,
..profile.clone()
};
let mut dep_namespace = match dependency_namespace(
&lib_namespace_map,
&compiled_contract_deps,
plan.graph(),
node,
&engines,
None,
experimental,
) {
Ok(o) => o,
Err(errs) => return fail(&[], &errs),
};
let compiled_without_tests = compile(
&descriptor,
&profile,
&engines,
&mut dep_namespace,
&mut source_map,
)?;
if let Some(outfile) = profile.metrics_outfile {
let path = Path::new(&outfile);
let metrics_json = serde_json::to_string(&compiled_without_tests.metrics)
.expect("JSON serialization failed");
fs::write(path, metrics_json)?;
}
if is_contract_dependency {
let compiled_contract_dep = CompiledContractDependency {
bytecode: compiled_without_tests.bytecode.bytes.clone(),
storage_slots: compiled_without_tests.storage_slots.clone(),
};
compiled_contract_deps.insert(node, compiled_contract_dep);
} else {
let contract_id = contract_id(
&compiled_without_tests.bytecode.bytes,
compiled_without_tests.storage_slots.clone(),
&fuel_tx::Salt::zeroed(),
);
contract_id_value = Some(format!("0x{contract_id}"));
}
Some(compiled_without_tests.bytecode)
} else {
None
};
let profile = if !plan.member_nodes().any(|member| member == node) {
BuildProfile {
include_tests: false,
..profile.clone()
}
} else {
profile.clone()
};
let mut dep_namespace = match dependency_namespace(
&lib_namespace_map,
&compiled_contract_deps,
plan.graph(),
node,
&engines,
contract_id_value.clone(),
experimental,
) {
Ok(o) => o,
Err(errs) => {
print_on_failure(
engines.se(),
profile.terse,
&[],
&errs,
profile.reverse_results,
);
bail!("Failed to compile {}", pkg.name);
}
};
let compiled = compile(
&descriptor,
&profile,
&engines,
&mut dep_namespace,
&mut source_map,
)?;
if let Some(outfile) = profile.metrics_outfile {
let path = Path::new(&outfile);
let metrics_json =
serde_json::to_string(&compiled.metrics).expect("JSON serialization failed");
fs::write(path, metrics_json)?;
}
if let TreeType::Library = compiled.tree_type {
lib_namespace_map.insert(node, compiled.root_module);
}
source_map.insert_dependency(descriptor.manifest_file.dir());
let built_pkg = BuiltPackage {
descriptor,
program_abi: compiled.program_abi,
storage_slots: compiled.storage_slots,
source_map: compiled.source_map,
tree_type: compiled.tree_type,
bytecode: compiled.bytecode,
warnings: compiled.warnings,
bytecode_without_tests,
};
if outputs.contains(&node) {
built_packages.push((node, built_pkg));
}
}
Ok(built_packages)
}
#[allow(clippy::too_many_arguments)]
pub fn check(
plan: &BuildPlan,
build_target: BuildTarget,
terse_mode: bool,
lsp_mode: Option<LspConfig>,
include_tests: bool,
engines: &Engines,
retrigger_compilation: Option<Arc<AtomicBool>>,
experimental: sway_core::ExperimentalFlags,
) -> anyhow::Result<Vec<(Option<Programs>, Handler)>> {
let mut lib_namespace_map = HashMap::default();
let mut source_map = SourceMap::new();
let compiled_contract_deps = HashMap::new();
let mut results = vec![];
for (idx, &node) in plan.compilation_order.iter().enumerate() {
let pkg = &plan.graph[node];
let manifest = &plan.manifest_map()[&pkg.id()];
const DUMMY_CONTRACT_ID: &str =
"0x0000000000000000000000000000000000000000000000000000000000000000";
let contract_id_value =
(idx == plan.compilation_order.len() - 1).then(|| DUMMY_CONTRACT_ID.to_string());
let mut dep_namespace = dependency_namespace(
&lib_namespace_map,
&compiled_contract_deps,
&plan.graph,
node,
engines,
contract_id_value,
experimental,
)
.expect("failed to create dependency namespace");
let profile = BuildProfile {
terse: terse_mode,
..BuildProfile::debug()
};
let build_config = sway_build_config(
manifest.dir(),
&manifest.entry_path(),
build_target,
&profile,
)?
.with_include_tests(include_tests)
.with_lsp_mode(lsp_mode.clone());
let input = manifest.entry_string()?;
let handler = Handler::default();
let programs_res = sway_core::compile_to_ast(
&handler,
engines,
input,
&mut dep_namespace,
Some(&build_config),
&pkg.name,
retrigger_compilation.clone(),
);
if retrigger_compilation
.as_ref()
.is_some_and(|b| b.load(std::sync::atomic::Ordering::SeqCst))
{
bail!("compilation was retriggered")
}
let programs = match programs_res.as_ref() {
Ok(programs) => programs,
_ => {
results.push((programs_res.ok(), handler));
return Ok(results);
}
};
if let Ok(typed_program) = programs.typed.as_ref() {
if let TreeType::Library = typed_program.kind.tree_type() {
let mut module = typed_program
.root
.namespace
.program_id(engines)
.read(engines, |m| m.clone());
module.set_span(
Span::new(
manifest.entry_string()?,
0,
0,
Some(engines.se().get_source_id(&manifest.entry_path())),
)
.unwrap(),
);
lib_namespace_map.insert(node, module);
}
source_map.insert_dependency(manifest.dir());
} else {
results.push((programs_res.ok(), handler));
return Ok(results);
}
results.push((programs_res.ok(), handler));
}
if results.is_empty() {
bail!("unable to check sway program: build plan contains no packages")
}
Ok(results)
}
pub fn manifest_file_missing<P: AsRef<Path>>(dir: P) -> anyhow::Error {
let message = format!(
"could not find `{}` in `{}` or any parent directory",
constants::MANIFEST_FILE_NAME,
dir.as_ref().display()
);
Error::msg(message)
}
pub fn parsing_failed(project_name: &str, errors: &[CompileError]) -> anyhow::Error {
let error = errors
.iter()
.map(|e| format!("{e}"))
.collect::<Vec<String>>()
.join("\n");
let message = format!("Parsing {project_name} failed: \n{error}");
Error::msg(message)
}
pub fn wrong_program_type(
project_name: &str,
expected_types: &[TreeType],
parse_type: TreeType,
) -> anyhow::Error {
let message = format!("{project_name} is not a '{expected_types:?}' it is a '{parse_type:?}'");
Error::msg(message)
}
pub fn fuel_core_not_running(node_url: &str) -> anyhow::Error {
let message = format!("could not get a response from node at the URL {node_url}. Start a node with `fuel-core`. See https://github.com/FuelLabs/fuel-core#running for more information");
Error::msg(message)
}
#[cfg(test)]
mod test {
use super::*;
use regex::Regex;
fn setup_build_plan() -> BuildPlan {
let current_dir = env!("CARGO_MANIFEST_DIR");
let manifest_dir = PathBuf::from(current_dir)
.parent()
.unwrap()
.join("test/src/e2e_vm_tests/test_programs/should_pass/forc/workspace_building/");
let manifest_file = ManifestFile::from_dir(manifest_dir).unwrap();
let member_manifests = manifest_file.member_manifests().unwrap();
let lock_path = manifest_file.lock_path().unwrap();
BuildPlan::from_lock_and_manifests(
&lock_path,
&member_manifests,
false,
false,
&IPFSNode::default(),
)
.unwrap()
}
#[test]
fn test_root_pkg_order() {
let build_plan = setup_build_plan();
let graph = build_plan.graph();
let order: Vec<String> = build_plan
.member_nodes()
.map(|order| graph[order].name.clone())
.collect();
assert_eq!(order, vec!["test_lib", "test_contract", "test_script"])
}
#[test]
fn test_visualize_with_url_prefix() {
let build_plan = setup_build_plan();
let result = build_plan.visualize(Some("some-prefix::".to_string()));
let re = Regex::new(r#"digraph \{
0 \[ label = "core" shape = box URL = "some-prefix::[[:ascii:]]+/sway-lib-core/Forc.toml"\]
1 \[ label = "test_contract" shape = box URL = "some-prefix::/[[:ascii:]]+/test_contract/Forc.toml"\]
2 \[ label = "test_lib" shape = box URL = "some-prefix::/[[:ascii:]]+/test_lib/Forc.toml"\]
3 \[ label = "test_script" shape = box URL = "some-prefix::/[[:ascii:]]+/test_script/Forc.toml"\]
3 -> 2 \[ \]
3 -> 0 \[ \]
3 -> 1 \[ \]
1 -> 2 \[ \]
1 -> 0 \[ \]
\}
"#).unwrap();
dbg!(&result);
assert!(!re.find(result.as_str()).unwrap().is_empty());
}
#[test]
fn test_visualize_without_prefix() {
let build_plan = setup_build_plan();
let result = build_plan.visualize(None);
let expected = r#"digraph {
0 [ label = "core" shape = box ]
1 [ label = "test_contract" shape = box ]
2 [ label = "test_lib" shape = box ]
3 [ label = "test_script" shape = box ]
3 -> 2 [ ]
3 -> 0 [ ]
3 -> 1 [ ]
1 -> 2 [ ]
1 -> 0 [ ]
}
"#;
assert_eq!(expected, result);
}
}