Skip to content

Commit

Permalink
feat: build granularity (#974)
Browse files Browse the repository at this point in the history
- `dfx build` now optionally takes a `canister_name` and optionally takes `--all` (`--all` and `canister_name` are mutually exclusive)
- builds direct and transitive dependencies specified in `dfx.json` i.e. if dfx.json has `"dependencies": ["{canister_B}"]` for a `canister_A`, `dfx build canister_A` builds `canister_A` and `canister_B`

# Breaking Changes
- removes `dfx build --skip-frontend` as it is no longer required
  • Loading branch information
p-shahi authored Sep 2, 2020
1 parent f2391e2 commit 93bf4b7
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 90 deletions.
5 changes: 5 additions & 0 deletions e2e/bats/assets/transitive_deps_canisters/a/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
actor {
public func greet(name : Text) : async Text {
return "Namaste, " # name # "!";
};
};
5 changes: 5 additions & 0 deletions e2e/bats/assets/transitive_deps_canisters/b/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
actor {
public func greet(name : Text) : async Text {
return "Hola, " # name # "!";
};
};
5 changes: 5 additions & 0 deletions e2e/bats/assets/transitive_deps_canisters/c/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
actor {
public func greet(name : Text) : async Text {
return "Hello, " # name # "!";
};
};
5 changes: 5 additions & 0 deletions e2e/bats/assets/transitive_deps_canisters/d/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
actor {
public func greet(name : Text) : async Text {
return "Hello, " # name # "!";
};
};
38 changes: 38 additions & 0 deletions e2e/bats/assets/transitive_deps_canisters/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"canisters": {
"canister_a": {
"main": "./a/main.mo",
"type": "motoko"
},
"canister_b": {
"dependencies": ["canister_a"],
"main": "./b/main.mo",
"type": "motoko"
},
"canister_c": {
"dependencies": ["canister_b"],
"main": "./c/main.mo",
"type": "motoko"
},
"canister_d": {
"dependencies": ["canister_e"],
"main": "./d/main.mo",
"type": "motoko"
},
"canister_e": {
"dependencies": ["canister_d"],
"main": "./e/main.mo",
"type": "motoko"
}
},
"defaults": {
"build": {
"packtool": ""
}
},
"networks": {
"local": {
"bind": "127.0.0.1:8000"
}
}
}
5 changes: 5 additions & 0 deletions e2e/bats/assets/transitive_deps_canisters/e/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
actor {
public func greet(name : Text) : async Text {
return "Hello, " # name # "!";
};
};
1 change: 1 addition & 0 deletions e2e/bats/assets/transitive_deps_canisters/patch.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Do nothing
96 changes: 96 additions & 0 deletions e2e/bats/build_granular.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bats

load utils/_

setup() {
# We want to work from a temporary directory, different for every test.
cd $(mktemp -d -t dfx-e2e-XXXXXXXX)

dfx_new
}

teardown() {
dfx_stop
}

@test "direct dependencies are built" {
dfx_start
dfx canister create --all
#specify build for only assets_canister
dfx build e2e_project_assets

#validate direct dependency built and is callable
assert_command dfx canister install e2e_project
assert_command dfx canister call e2e_project greet World
}

@test "transitive dependencies are built" {
install_asset transitive_deps_canisters
dfx_start
dfx canister create --all
#install of tertiary dependency canister will fail since its not built
assert_command_fail dfx canister install canister_a
#specify build for primary canister
dfx build canister_c

#validate tertiary transitive dependency is built and callable
assert_command dfx canister install canister_a
assert_command dfx canister call canister_a greet World
assert_match '("Namaste, World!")'
}

@test "unspecified dependencies are not built" {
dfx_start
dfx canister create --all
# only build motoko canister
dfx build e2e_project
# validate assets canister wasn't built and can't be installed
assert_command_fail dfx canister install e2e_project_assets
assert_match "No such file or directory"
}


@test "manual build of specified canisters succeeds" {
install_asset assetscanister

dfx_start
dfx canister create e2e_project
dfx build e2e_project
assert_command dfx canister install e2e_project
assert_command dfx canister call e2e_project greet World

assert_command_fail dfx canister install e2e_project_assets
assert_match "Cannot find canister id. Please issue 'dfx canister create e2e_project_assets'."
dfx canister create e2e_project_assets
dfx build e2e_project_assets
dfx canister install e2e_project_assets

assert_command dfx canister call --query e2e_project_assets retrieve '("binary/noise.txt")'
assert_eq '(vec { 184; 1; 32; 128; 10; 119; 49; 50; 32; 0; 120; 121; 10; 75; 76; 11; 10; 106; 107; })'

assert_command dfx canister call --query e2e_project_assets retrieve '("text-with-newlines.txt")'
assert_eq '(vec { 99; 104; 101; 114; 114; 105; 101; 115; 10; 105; 116; 39; 115; 32; 99; 104; 101; 114; 114; 121; 32; 115; 101; 97; 115; 111; 110; 10; 67; 72; 69; 82; 82; 73; 69; 83; })'

}

@test "cyclic dependencies are detected" {
install_asset transitive_deps_canisters
dfx_start
dfx canister create --all
assert_command dfx build canister_e
assert_match "Possible circular dependency detected during evaluation of canister_d's dependency on canister_e."
}

@test "the all flag builds everything" {
dfx_start
dfx canister create --all
assert_command dfx build --all
assert_command dfx canister install --all
}


@test "the all flags conflicts with canister name" {
dfx_start
dfx canister create --all
assert_command_fail dfx build e2e_project --all
}
2 changes: 1 addition & 1 deletion e2e/bats/create.bash
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ teardown() {
@test "build fails without create" {
dfx_start
assert_command_fail dfx build
assert_match "Cannot find canister id. Please issue 'dfx canister create e2e_project'"
assert_match "Cannot find canister id."
}

@test "build fails if all canisters in project are not created" {
Expand Down
4 changes: 2 additions & 2 deletions e2e/bats/frontend.bash
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ teardown() {
@test "dfx start serves a frontend" {
dfx_start
dfx canister create --all
dfx build --skip-frontend
dfx build e2e_project

sleep 1
assert_command curl http://localhost:8000 # 8000 = default port.
Expand All @@ -31,7 +31,7 @@ teardown() {
cat <<<$(jq '.networks.local.bind="127.0.0.1:12345"' dfx.json) >dfx.json

dfx canister create --all
dfx build --skip-frontend
dfx build e2e_project

assert_command curl http://localhost:12345 # 8000 = default port.
assert_match "<html>"
Expand Down
28 changes: 19 additions & 9 deletions src/dfx/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ pub fn construct() -> App<'static, 'static> {
SubCommand::with_name("build")
.about(UserMessage::BuildCanister.to_str())
.arg(
Arg::with_name("skip-frontend")
.long("skip-frontend")
.takes_value(false)
.help(UserMessage::SkipFrontend.to_str()),
Arg::with_name("canister_name")
.takes_value(true)
.conflicts_with("all")
.help(UserMessage::BuildCanisterName.to_str())
.required(false),
)
.arg(
Arg::with_name("all")
.long("all")
.conflicts_with("canister_name")
.help(UserMessage::BuildAll.to_str())
.takes_value(false),
)
.arg(
Arg::with_name("check")
Expand Down Expand Up @@ -45,8 +53,12 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult {
env.get_cache().install()?;

let build_mode_check = args.is_present("check");
// First build.
let canister_pool = CanisterPool::load(&env, build_mode_check)?;

// Option can be None in which case --all was specified
let some_canister = args.value_of("canister_name");

// Get pool of canisters to build
let canister_pool = CanisterPool::load(&env, build_mode_check, some_canister)?;

// Create canisters on the replica and associate canister ids locally.
if args.is_present("check") {
Expand All @@ -66,9 +78,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult {
slog::info!(logger, "Building canisters...");

canister_pool.build_or_fail(
BuildConfig::from_config(&config)?
.with_skip_frontend(args.is_present("skip-frontend"))
.with_build_mode_check(build_mode_check),
BuildConfig::from_config(&config)?.with_build_mode_check(build_mode_check),
)?;

Ok(())
Expand Down
73 changes: 31 additions & 42 deletions src/dfx/src/lib/builders/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,43 +104,36 @@ impl CanisterBuilder for AssetsBuilder {
info: &CanisterInfo,
config: &BuildConfig,
) -> DfxResult {
if !config.skip_frontend {
let deps = match info.get_extra_value("dependencies") {
None => vec![],
Some(v) => Vec::<String>::deserialize(v).map_err(|_| {
DfxError::Unknown(String::from("Field 'dependencies' is of the wrong type"))
})?,
};
let dependencies = deps
.iter()
.map(|name| {
pool.get_first_canister_with_name(name)
.map(|c| c.canister_id())
.map_or_else(
|| Err(DfxError::UnknownCanisterNamed(name.clone())),
DfxResult::Ok,
)
})
.collect::<DfxResult<Vec<CanisterId>>>()?;

build_frontend(
pool.get_logger(),
info.get_workspace_root(),
&config.network_name,
dependencies,
pool,
)?;
}
let deps = match info.get_extra_value("dependencies") {
None => vec![],
Some(v) => Vec::<String>::deserialize(v).map_err(|_| {
DfxError::Unknown(String::from("Field 'dependencies' is of the wrong type"))
})?,
};
let dependencies = deps
.iter()
.map(|name| {
pool.get_first_canister_with_name(name)
.map(|c| c.canister_id())
.map_or_else(
|| Err(DfxError::UnknownCanisterNamed(name.clone())),
DfxResult::Ok,
)
})
.collect::<DfxResult<Vec<CanisterId>>>()?;

let assets_canister_info = info.as_info::<AssetsCanisterInfo>()?;
if !config.skip_frontend {
assets_canister_info.assert_source_paths()?;
}
copy_assets(
build_frontend(
pool.get_logger(),
&assets_canister_info,
config.skip_frontend,
info.get_workspace_root(),
&config.network_name,
dependencies,
pool,
)?;

let assets_canister_info = info.as_info::<AssetsCanisterInfo>()?;
assets_canister_info.assert_source_paths()?;

copy_assets(pool.get_logger(), &assets_canister_info)?;
Ok(())
}
}
Expand Down Expand Up @@ -170,20 +163,16 @@ fn delete_output_directory(
Ok(())
}

fn copy_assets(
logger: &slog::Logger,
assets_canister_info: &AssetsCanisterInfo,
skip_frontend: bool,
) -> DfxResult {
fn copy_assets(logger: &slog::Logger, assets_canister_info: &AssetsCanisterInfo) -> DfxResult {
let source_paths = assets_canister_info.get_source_paths();
let output_assets_path = assets_canister_info.get_output_assets_path();

for source_path in source_paths {
// If we skip-frontend and the source doesn't exist, we ignore it.
if skip_frontend && !source_path.exists() {
// If the source doesn't exist, we ignore it.
if !source_path.exists() {
slog::warn!(
logger,
r#"Skip copying "{}" because --skip-frontend was used."#,
r#"Source path "{}" does not exist."#,
source_path.to_string_lossy()
);

Expand Down
9 changes: 0 additions & 9 deletions src/dfx/src/lib/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ pub trait CanisterBuilder {
#[derive(Clone)]
pub struct BuildConfig {
profile: Profile,
pub skip_frontend: bool,
pub build_mode_check: bool,
pub network_name: String,

Expand All @@ -98,20 +97,12 @@ impl BuildConfig {
Ok(BuildConfig {
network_name,
profile: config_intf.profile.unwrap_or(Profile::Debug),
skip_frontend: false,
build_mode_check: false,
build_root: build_root.clone(),
idl_root: build_root.join("idl/"),
})
}

pub fn with_skip_frontend(self, skip_frontend: bool) -> Self {
Self {
skip_frontend,
..self
}
}

pub fn with_build_mode_check(self, build_mode_check: bool) -> Self {
Self {
build_mode_check,
Expand Down
3 changes: 2 additions & 1 deletion src/dfx/src/lib/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ user_message!(
RequestId => "Specifies the request identifier. The request identifier is an hexadecimal string starting with 0x.",

// dfx build
BuildAll => "Builds all canisters configured in dfx.json.",
BuildCanisterName => "Specifies the canister name. Either this or the --all flag are required.",
BuildCanister => "Builds all or specific canisters from the code in your project. By default, all canisters are built.",
SkipFrontend => "Skip building the frontend, only build the canisters.",
BuildCheck => "Build canisters without creating them. This can be used to check that canisters build ok.",
CanisterComputeNetwork => "Override the compute network to connect to. By default uses the local network.",

Expand Down
Loading

0 comments on commit 93bf4b7

Please sign in to comment.