Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sozo): add full workspace and accept non-dojo targets #2207

Merged
merged 13 commits into from
Jul 25, 2024
45 changes: 23 additions & 22 deletions bin/sozo/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ use clap::{Args, Parser};
use dojo_bindgen::{BuiltinPlugins, PluginManager};
use dojo_lang::scarb_internal::compile_workspace;
use dojo_world::manifest::MANIFESTS_DIR;
use dojo_world::metadata::dojo_metadata_from_workspace;
use dojo_world::metadata::{dojo_metadata_from_package, dojo_metadata_from_workspace};
use prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE;
use prettytable::{format, Cell, Row, Table};
use scarb::core::{Config, TargetKind};
use scarb::core::{Config, Package, TargetKind};
use scarb::ops::CompileOpts;
use scarb_ui::args::FeaturesSpec;
use scarb_ui::args::{FeaturesSpec, PackagesFilter};
use sozo_ops::statistics::{get_contract_statistics_for_dir, ContractStatistics};
use tracing::trace;

Expand Down Expand Up @@ -43,39 +43,39 @@ pub struct BuildArgs {
/// Specify the features to activate.
#[command(flatten)]
pub features: FeaturesSpec,

/// Specify packages to build.
#[command(flatten)]
pub packages: Option<PackagesFilter>,
}

impl BuildArgs {
pub fn run(self, config: &Config) -> Result<()> {
let ws = scarb::ops::read_workspace(config.manifest_path(), config)?;

if let Ok(current_package) = ws.current_package() {
if current_package.target(&TargetKind::new("dojo")).is_none() {
return Err(anyhow::anyhow!(
"No Dojo target found in the {} package. Add [[target.dojo]] to the {} \
manifest to enable Dojo features and compile with sozo.",
current_package.id.to_string(),
current_package.manifest_path()
));
}
}
let packages: Vec<Package> = if let Some(filter) = self.packages {
filter.match_many(&ws)?.into_iter().collect()
} else {
ws.members().collect()
};

// Namespaces are required to compute contracts/models data. Hence, we can't continue
// if no metadata are found.
// Once sozo will support package option, users will be able to do `-p` to select
// the package directly from the workspace instead of using `--manifest-path`.
let dojo_metadata = dojo_metadata_from_workspace(&ws)?;
let dojo_metadata = if packages.len() == 1 {
let package = packages.first().unwrap();
dojo_metadata_from_package(package, &ws)?
} else {
dojo_metadata_from_workspace(&ws)?
};

let profile_name =
ws.current_profile().expect("Scarb profile is expected at this point.").to_string();

// Manifest path is always a file, we can unwrap safely to get the
// parent folder.
// Manifest path is always a file, we can unwrap safely to get the parent folder.
let manifest_dir = ws.manifest_path().parent().unwrap().to_path_buf();

let profile_dir = manifest_dir.join(MANIFESTS_DIR).join(profile_name);
CleanArgs::clean_manifests(&profile_dir)?;
let packages: Vec<scarb::core::PackageId> = ws.members().map(|p| p.id).collect();

trace!(?packages);

let compile_info = compile_workspace(
config,
Expand All @@ -85,7 +85,7 @@ impl BuildArgs {
exclude_target_kinds: vec![TargetKind::TEST],
features: self.features.try_into()?,
},
packages,
packages.iter().map(|p| p.id).collect(),
)?;
trace!(?compile_info, "Compiled workspace.");

Expand Down Expand Up @@ -167,6 +167,7 @@ impl Default for BuildArgs {
unity: false,
bindings_output: "bindings".to_string(),
stats: false,
packages: None,
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion bin/sozo/src/commands/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use scarb::compiler::helpers::collect_main_crate_ids;
use scarb::compiler::{CairoCompilationUnit, CompilationUnit, CompilationUnitAttributes};
use scarb::core::{Config, TargetKind};
use scarb::ops::{self, CompileOpts};
use scarb_ui::args::FeaturesSpec;
use scarb_ui::args::{FeaturesSpec, PackagesFilter};
use tracing::trace;

pub(crate) const LOG_TARGET: &str = "sozo::cli::commands::test";
Expand Down Expand Up @@ -62,6 +62,9 @@ pub struct TestArgs {
/// Specify the features to activate.
#[command(flatten)]
features: FeaturesSpec,
/// Specify packages to test.
#[command(flatten)]
pub packages: Option<PackagesFilter>,
}

impl TestArgs {
Expand All @@ -87,6 +90,8 @@ impl TestArgs {
opts.include_target_kinds.is_empty()
|| opts.include_target_kinds.contains(&cu.main_component().target_kind())
})
// TODOL: Need to find how to filter from packages with the compilation unit. We need something
// implementing PackagesSource trait.
.collect::<Vec<_>>();

for unit in compilation_units {
Expand Down
2 changes: 2 additions & 0 deletions crates/dojo-lang/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ impl Compiler for DojoCompiler {

let compiler_config = build_compiler_config(&unit, ws);

trace!(target: LOG_TARGET, unit = %unit.name(), ?props, "Compiling unit dojo compiler.");

let mut main_crate_ids = collect_main_crate_ids(&unit, db);
let core_crate_ids: Vec<CrateId> = collect_core_crate_ids(db);
main_crate_ids.extend(core_crate_ids);
Expand Down
11 changes: 10 additions & 1 deletion crates/dojo-lang/src/scarb_internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use cairo_lang_starknet::starknet_plugin_suite;
use cairo_lang_test_plugin::test_plugin_suite;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use camino::Utf8PathBuf;
use regex::Regex;
use scarb::compiler::{CairoCompilationUnit, CompilationUnit, CompilationUnitAttributes};
use scarb::core::{Config, PackageId};
use scarb::ops::CompileOpts;
Expand Down Expand Up @@ -90,6 +91,7 @@ pub fn compile_workspace(
) -> Result<CompileInfo> {
let ws = scarb::ops::read_workspace(config.manifest_path(), config)?;
let resolve = scarb::ops::resolve_workspace(&ws)?;
let ui = config.ui();

let compilation_units = scarb::ops::generate_compilation_units(&resolve, &opts.features, &ws)?
.into_iter()
Expand All @@ -103,9 +105,16 @@ pub fn compile_workspace(

let mut compile_error_units = vec![];
for unit in compilation_units {
trace!(target: LOG_TARGET, unit_name = %unit.name(), target_kind = %unit.main_component().target_kind(), "Compiling unit.");
if let CompilationUnit::Cairo(unit) = unit {
let mut db = build_scarb_root_database(&unit).unwrap();
let unit_name = unit.name();
let re = Regex::new(r"\s*\([^()]*\)$").unwrap();
let unit_name_no_path = re.replace(&unit_name, "");

ui.print(format!("compiling {}", unit_name_no_path));
ui.verbose(format!("target kind: {}", unit.main_component().target_kind()));

let mut db = build_scarb_root_database(&unit).unwrap();
if let Err(err) = ws.config().compilers().compile(unit.clone(), &mut (db), &ws) {
ws.config().ui().anyhow(&err);
compile_error_units.push(unit.name());
Expand Down
22 changes: 21 additions & 1 deletion crates/dojo-lang/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ pub fn is_name_valid(name: &str) -> bool {
/// Get the namespace configuration from the workspace.
// TODO: Ask to Scarb team to expose this information with the new macro system.
pub fn get_namespace_config(db: &dyn SyntaxGroup) -> Result<NamespaceConfig> {
// Super verbose print, but useful to get the CfgSet.
// debug!(cfg_set = ?db.cfg_set(), crates = ?db.crates(), "Retrieving namespace
// configuration.");

if !db.cfg_set().contains(&cairo_lang_filesystem::cfg::Cfg {
key: "target".into(),
value: Some("dojo".into()),
}) {
// When a [lib] is compiled without the target "dojo", we shouldn't care about
// the namespace being retrieved.
return Ok(NamespaceConfig { default: "ignored_namespace".into(), mappings: None });
}

let crates = db.crates();

if crates.is_empty() {
Expand All @@ -26,13 +39,20 @@ pub fn get_namespace_config(db: &dyn SyntaxGroup) -> Result<NamespaceConfig> {
// Crates[0] is always the root crate that triggered the build origin.
// In case of a library, crates[0] refers to the lib itself if compiled directly,
// or the crate using the library otherwise.
let configuration = match db.crate_config(crates[0]) {
let configuration = match db
.crate_config(*crates.first().expect("No root crate found in the workspace."))
{
Option::Some(cfg) => cfg,
Option::None => return Err(anyhow::anyhow!("No configuration found for the root crate.")),
};

if let Directory::Real(ref path) = configuration.root {
let config_path = path.parent().unwrap().join("Scarb.toml");

// Very verbose.
// tracing::debug!(config_path = %config_path.to_string_lossy(), "Reading Scarb.toml file
// for namespace config.");

let config_content = match fs::read_to_string(&config_path) {
Ok(x) => x,
Err(e) => return Err(anyhow::anyhow!("Failed to read Scarb.toml file: {e}.")),
Expand Down
152 changes: 50 additions & 102 deletions crates/dojo-world/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ use std::collections::HashMap;
use std::io::Cursor;
use std::path::PathBuf;

use anyhow::{anyhow, Context, Result};
use camino::Utf8PathBuf;
use anyhow::{Context, Result};
use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri};
use regex::Regex;
use scarb::core::{ManifestMetadata, Workspace};
use scarb::core::{ManifestMetadata, Package, TargetKind, Workspace};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;
use url::Url;

use crate::contracts::naming;
use crate::manifest::{BaseManifest, CONTRACTS_DIR, MODELS_DIR, WORLD_CONTRACT_TAG};
const LOG_TARGET: &str = "dojo_world::metadata";

#[cfg(test)]
#[path = "metadata_test.rs"]
Expand All @@ -27,24 +25,6 @@ pub const MANIFESTS_DIR: &str = "manifests";
pub const ABIS_DIR: &str = "abis";
pub const BASE_DIR: &str = "base";

fn build_artifact_from_filename(
abi_dir: &Utf8PathBuf,
source_dir: &Utf8PathBuf,
filename: &str,
) -> ArtifactMetadata {
let abi_file = abi_dir.join(format!("{filename}.json"));
let src_file = source_dir.join(format!("{filename}.cairo"));

ArtifactMetadata {
abi: if abi_file.exists() { Some(Uri::File(abi_file.into_std_path_buf())) } else { None },
source: if src_file.exists() {
Some(Uri::File(src_file.into_std_path_buf()))
} else {
None
},
}
}

/// Get the default namespace from the workspace.
///
/// # Arguments
Expand Down Expand Up @@ -96,94 +76,62 @@ pub fn project_to_world_metadata(m: ProjectWorldMetadata) -> WorldMetadata {
}
}

/// Collect metadata from the project configuration and from the workspace.
///
/// # Arguments
/// `ws`: the workspace.
///
/// # Returns
/// A [`DojoMetadata`] object containing all Dojo metadata.
pub fn dojo_metadata_from_workspace(ws: &Workspace<'_>) -> Result<DojoMetadata> {
let profile = ws.config().profile();

let manifest_dir = ws.manifest_path().parent().unwrap().to_path_buf();
let manifest_dir = manifest_dir.join(MANIFESTS_DIR).join(profile.as_str());
let abi_dir = manifest_dir.join(BASE_DIR).join(ABIS_DIR);
let source_dir = ws.target_dir().path_existent().unwrap();
let source_dir = source_dir.join(profile.as_str());

let project_metadata = if let Ok(current_package) = ws.current_package() {
current_package
.manifest
.metadata
.dojo()
.with_context(|| format!("Error parsing manifest file `{}`", ws.manifest_path()))?
} else {
// On workspaces, dojo metadata are not accessible because if no current package is defined
// (being the only package or using --package).
return Err(anyhow!(
"No current package with dojo metadata found, virtual manifest in workspace are not \
supported. Until package compilation is supported, you will have to provide the path \
to the Scarb.toml file using the --manifest-path option."
));
};
pub fn dojo_metadata_from_package(package: &Package, ws: &Workspace<'_>) -> Result<DojoMetadata> {
tracing::debug!(target: LOG_TARGET, package_id = package.id.to_string(), "Collecting Dojo metadata from package.");

if package.target(&TargetKind::new("lib")).is_some()
|| package.target(&TargetKind::new("dojo")).is_none()
{
return Ok(DojoMetadata::default());
}

let project_metadata = package
.manifest
.metadata
.dojo()
.with_context(|| format!("Error parsing manifest file `{}`", ws.manifest_path()))?;

let mut dojo_metadata = DojoMetadata {
let dojo_metadata = DojoMetadata {
env: project_metadata.env.clone(),
skip_migration: project_metadata.skip_migration.clone(),
world: project_to_world_metadata(project_metadata.world),
..Default::default()
};

let world_artifact = build_artifact_from_filename(
&abi_dir,
&source_dir,
&naming::get_filename_from_tag(WORLD_CONTRACT_TAG),
);

// inialize Dojo world metadata with world metadata coming from project configuration
dojo_metadata.world = project_to_world_metadata(project_metadata.world);
dojo_metadata.world.artifacts = world_artifact;

// load models and contracts metadata
if manifest_dir.join(BASE_DIR).exists() {
if let Ok(manifest) = BaseManifest::load_from_path(&manifest_dir.join(BASE_DIR)) {
for model in manifest.models {
let tag = model.inner.tag.clone();
let abi_model_dir = abi_dir.join(MODELS_DIR);
let source_model_dir = source_dir.join(MODELS_DIR);
dojo_metadata.resources_artifacts.insert(
tag.clone(),
ResourceMetadata {
name: tag.clone(),
artifacts: build_artifact_from_filename(
&abi_model_dir,
&source_model_dir,
&naming::get_filename_from_tag(&tag),
),
},
);
}
tracing::trace!(target: LOG_TARGET, ?dojo_metadata);

for contract in manifest.contracts {
let tag = contract.inner.tag.clone();
let abi_contract_dir = abi_dir.join(CONTRACTS_DIR);
let source_contract_dir = source_dir.join(CONTRACTS_DIR);
dojo_metadata.resources_artifacts.insert(
tag.clone(),
ResourceMetadata {
name: tag.clone(),
artifacts: build_artifact_from_filename(
&abi_contract_dir,
&source_contract_dir,
&naming::get_filename_from_tag(&tag),
),
},
);
}
Ok(dojo_metadata)
}
Comment on lines +125 to +126
Copy link

Choose a reason for hiding this comment

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

Ohayo, sensei! Consider logging the collected metadata at a higher log level.

Using info level instead of trace might be more appropriate for logging collected metadata.

- tracing::trace!(target: LOG_TARGET, ?dojo_metadata);
+ tracing::info!(target: LOG_TARGET, ?dojo_metadata, "Collected Dojo metadata from package.");

Committable suggestion was skipped due to low confidence.


pub fn dojo_metadata_from_workspace(ws: &Workspace<'_>) -> Result<DojoMetadata> {
let dojo_packages: Vec<Package> = ws
.members()
.filter(|package| {
package.target(&TargetKind::new("dojo")).is_some()
&& package.target(&TargetKind::new("lib")).is_none()
})
.collect();

match dojo_packages.len() {
0 => {
ws.config().ui().warn(
"No package with dojo target found in workspace. If your package is a [lib] with \
[[target.dojo]], you can ignore this warning.",
);
Ok(DojoMetadata::default())
}
1 => {
let dojo_package =
dojo_packages.into_iter().next().expect("Package must exist as len is 1.");
Ok(dojo_metadata_from_package(&dojo_package, ws)?)
}
_ => {
Err(anyhow::anyhow!(
"Multiple packages with dojo target found in workspace. Please specify a package \
using --package option or maybe one of them must be declared as a [lib]."
))
}
}

Ok(dojo_metadata)
}

/// Metadata coming from project configuration (Scarb.toml)
Expand Down
Loading