Skip to content

Commit 5a2affe

Browse files
Add pre-install hooks to canisters.
1 parent 64b037d commit 5a2affe

File tree

10 files changed

+129
-33
lines changed

10 files changed

+129
-33
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ Your principal for ICP wallets and decentralized exchanges: ueuar-wxbnk-bdcsr-dn
8686
(run `dfx identity get-principal` to display)
8787
```
8888

89+
### feat: Add pre-install tasks
90+
91+
Add pre-install tasks, which can be defined by the new `pre-install` key for canister objects in `dfx.json` with a command or list of commands.
92+
8993
## Dependencies
9094

9195
### Frontend canister

docs/dfx-json-schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,16 @@
496496
}
497497
]
498498
},
499+
"pre_install": {
500+
"title": "Pre-Install Commands",
501+
"description": "One or more commands to run pre canister installation. These commands are executed in the root of the project.",
502+
"default": [],
503+
"allOf": [
504+
{
505+
"$ref": "#/definitions/SerdeVec_for_String"
506+
}
507+
]
508+
},
499509
"pullable": {
500510
"title": "Pullable",
501511
"description": "Defines required properties so that this canister is ready for `dfx deps pull` by other projects.",
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
{
22
"version": 1,
33
"canisters": {
4+
"preinstall": {
5+
"main": "main.mo",
6+
"pre_install": "echo hello-pre-file"
7+
},
8+
"preinstall_script": {
9+
"main": "main.mo",
10+
"pre_install": "preinstall.sh",
11+
"dependencies": ["preinstall"]
12+
},
413
"postinstall": {
514
"main": "main.mo",
6-
"post_install": "echo hello-file"
15+
"post_install": "echo hello-post-file"
716
},
817
"postinstall_script": {
918
"main": "main.mo",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env bash
22
echo "working directory of post-install script: '$(pwd)'"
3-
echo hello-script
3+
echo hello-post-script
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
echo "working directory of pre-install script: '$(pwd)'"
3+
echo hello-pre-script

e2e/tests-dfx/install.bash

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,69 +90,94 @@ teardown() {
9090
assert_command_fail dfx canister install --all --wasm "${archive:?}/wallet/0.10.0/wallet.wasm"
9191
}
9292

93-
@test "install runs post-install tasks" {
94-
install_asset post_install
93+
@test "install runs pre-post-install tasks" {
94+
install_asset pre_post_install
9595
dfx_start
9696

9797
assert_command dfx canister create --all
9898
assert_command dfx build
9999

100+
assert_command dfx canister install preinstall
101+
assert_match 'hello-pre-file'
102+
103+
assert_command dfx canister install preinstall_script
104+
assert_match 'hello-pre-script'
105+
106+
echo 'return 1' >> preinstall.sh
107+
assert_command_fail dfx canister install preinstall_script --mode upgrade
108+
assert_match 'hello-pre-script'
109+
100110
assert_command dfx canister install postinstall
101-
assert_match 'hello-file'
111+
assert_match 'hello-post-file'
102112

103113
assert_command dfx canister install postinstall_script
104-
assert_match 'hello-script'
114+
assert_match 'hello-post-script'
105115

106116
echo 'return 1' >> postinstall.sh
107117
assert_command_fail dfx canister install postinstall_script --mode upgrade
108-
assert_match 'hello-script'
118+
assert_match 'hello-post-script'
109119
}
110120

111-
@test "post-install tasks run in project root" {
112-
install_asset post_install
121+
@test "pre-post-install tasks run in project root" {
122+
install_asset pre_post_install
113123
dfx_start
114124

115125
assert_command dfx canister create --all
116126
assert_command dfx build
117127

118128
cd src/e2e_project_backend
119129

130+
assert_command dfx canister install preinstall_script
131+
assert_match 'hello-pre-script'
132+
assert_match "working directory of pre-install script: '.*/working-dir/e2e_project'"
133+
120134
assert_command dfx canister install postinstall_script
121-
assert_match 'hello-script'
135+
assert_match 'hello-post-script'
122136
assert_match "working directory of post-install script: '.*/working-dir/e2e_project'"
123137
}
124138

125-
@test "post-install tasks receive environment variables" {
126-
install_asset post_install
139+
@test "pre-post-install tasks receive environment variables" {
140+
install_asset pre_post_install
127141
dfx_start
142+
echo "echo hello \$CANISTER_ID" >> preinstall.sh
128143
echo "echo hello \$CANISTER_ID" >> postinstall.sh
129144

130145
assert_command dfx canister create --all
131146
assert_command dfx build
132-
id=$(dfx canister id postinstall_script)
147+
id_pre=$(dfx canister id preinstall_script)
148+
id_post=$(dfx canister id postinstall_script)
133149

134150
assert_command dfx canister install --all
135-
assert_match "hello $id"
151+
assert_match "hello $id_post"
152+
assert_command dfx canister install preinstall_script --mode upgrade
153+
assert_match "hello $id_pre"
136154
assert_command dfx canister install postinstall_script --mode upgrade
137-
assert_match "hello $id"
155+
assert_match "hello $id_post"
138156

139157
assert_command dfx deploy
140-
assert_match "hello $id"
158+
assert_match "hello $id_post"
159+
assert_command dfx deploy preinstall_script
160+
assert_match "hello $id_pre"
141161
assert_command dfx deploy postinstall_script
142-
assert_match "hello $id"
162+
assert_match "hello $id_post"
143163
}
144164

145-
@test "post-install tasks discover dependencies" {
146-
install_asset post_install
165+
@test "pre-post-install tasks discover dependencies" {
166+
install_asset pre_post_install
147167
dfx_start
168+
echo "echo hello \$CANISTER_ID_PREINSTALL" >> preinstall.sh
148169
echo "echo hello \$CANISTER_ID_POSTINSTALL" >> postinstall.sh
149170

150171
assert_command dfx canister create --all
151172
assert_command dfx build
152-
id=$(dfx canister id postinstall)
173+
id_pre=$(dfx canister id preinstall)
174+
id_post=$(dfx canister id postinstall)
175+
176+
assert_command dfx canister install preinstall_script
177+
assert_match "hello $id_pre"
153178

154179
assert_command dfx canister install postinstall_script
155-
assert_match "hello $id"
180+
assert_match "hello $id_post"
156181
}
157182

158183
@test "can install gzip wasm" {

src/dfx-core/src/config/model/dfinity.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,12 @@ pub struct ConfigCanistersCanister {
271271
#[serde(flatten)]
272272
pub type_specific: CanisterTypeProperties,
273273

274+
/// # Pre-Install Commands
275+
/// One or more commands to run pre canister installation.
276+
/// These commands are executed in the root of the project.
277+
#[serde(default)]
278+
pub pre_install: SerdeVec<String>,
279+
274280
/// # Post-Install Commands
275281
/// One or more commands to run post canister installation.
276282
/// These commands are executed in the root of the project.

src/dfx/src/lib/canister_info.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub struct CanisterInfo {
4949
type_specific: CanisterTypeProperties,
5050

5151
dependencies: Vec<String>,
52+
pre_install: Vec<String>,
5253
post_install: Vec<String>,
5354
main: Option<PathBuf>,
5455
shrink: Option<bool>,
@@ -171,6 +172,7 @@ impl CanisterInfo {
171172
_ => build_defaults.get_args(),
172173
};
173174

175+
let pre_install = canister_config.pre_install.clone().into_vec();
174176
let post_install = canister_config.post_install.clone().into_vec();
175177
let metadata = CanisterMetadataConfig::new(&canister_config.metadata, &network_name);
176178

@@ -190,6 +192,7 @@ impl CanisterInfo {
190192
args,
191193
type_specific,
192194
dependencies,
195+
pre_install,
193196
post_install,
194197
main: canister_config.main.clone(),
195198
shrink: canister_config.shrink,
@@ -262,6 +265,10 @@ impl CanisterInfo {
262265
&self.packtool
263266
}
264267

268+
pub fn get_pre_install(&self) -> &[String] {
269+
&self.pre_install
270+
}
271+
265272
pub fn get_post_install(&self) -> &[String] {
266273
&self.post_install
267274
}

src/dfx/src/lib/operations/canister/install_canister.rs

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ pub async fn install_canister(
5555
let log = env.get_logger();
5656
let agent = env.get_agent();
5757
let network = env.get_network_descriptor();
58+
if !canister_info.get_pre_install().is_empty() {
59+
let config = env.get_config()?;
60+
run_customized_install_tasks(
61+
env,
62+
canister_info,
63+
true,
64+
network,
65+
pool,
66+
env_file.or_else(|| config.as_ref()?.get_config().output_env_file.as_deref()),
67+
)?;
68+
}
5869
if !network.is_ic && named_canister::get_ui_canister_id(canister_id_store).is_none() {
5970
named_canister::install_ui_canister(env, canister_id_store, None).await?;
6071
}
@@ -282,9 +293,10 @@ The command line value will be used.",
282293
}
283294
if !canister_info.get_post_install().is_empty() {
284295
let config = env.get_config()?;
285-
run_post_install_tasks(
296+
run_customized_install_tasks(
286297
env,
287298
canister_info,
299+
false,
288300
network,
289301
pool,
290302
env_file.or_else(|| config.as_ref()?.get_config().output_env_file.as_deref()),
@@ -435,23 +447,26 @@ fn check_stable_compatibility(
435447
})
436448
}
437449

438-
#[context("Failed to run post-install tasks")]
439-
fn run_post_install_tasks(
450+
#[context("Failed to run {}-install tasks", if is_pre_install { "pre" } else { "post" })]
451+
fn run_customized_install_tasks(
440452
env: &dyn Environment,
441453
canister: &CanisterInfo,
454+
is_pre_install: bool,
442455
network: &NetworkDescriptor,
443456
pool: Option<&CanisterPool>,
444457
env_file: Option<&Path>,
445458
) -> DfxResult {
459+
let pre_or_post = if is_pre_install { "pre" } else { "post" };
446460
let tmp;
447461
let pool = match pool {
448462
Some(pool) => pool,
449463
None => {
450464
let config = env.get_config_or_anyhow()?;
451465
let canisters_to_load = all_project_canisters_with_ids(env, &config);
452466

453-
tmp = CanisterPool::load(env, false, &canisters_to_load)
454-
.context("Error collecting canisters for post-install task")?;
467+
tmp = CanisterPool::load(env, false, &canisters_to_load).context(format!(
468+
"Error collecting canisters for {pre_or_post}-install task"
469+
))?;
455470
&tmp
456471
}
457472
};
@@ -460,27 +475,43 @@ fn run_post_install_tasks(
460475
.iter()
461476
.map(|can| can.canister_id())
462477
.collect_vec();
463-
for task in canister.get_post_install() {
464-
run_post_install_task(canister, task, network, pool, &dependencies, env_file)?;
478+
let tasks = if is_pre_install {
479+
canister.get_pre_install()
480+
} else {
481+
canister.get_post_install()
482+
};
483+
for task in tasks {
484+
run_customized_install_task(
485+
canister,
486+
task,
487+
is_pre_install,
488+
network,
489+
pool,
490+
&dependencies,
491+
env_file,
492+
)?;
465493
}
466494
Ok(())
467495
}
468496

469-
#[context("Failed to run post-install task {task}")]
470-
fn run_post_install_task(
497+
#[context("Failed to run {}-install task {}", if is_pre_install { "pre" } else { "post" }, task)]
498+
fn run_customized_install_task(
471499
canister: &CanisterInfo,
472500
task: &str,
501+
is_pre_install: bool,
473502
network: &NetworkDescriptor,
474503
pool: &CanisterPool,
475504
dependencies: &[Principal],
476505
env_file: Option<&Path>,
477506
) -> DfxResult {
507+
let pre_or_post = if is_pre_install { "pre" } else { "post" };
478508
let cwd = canister.get_workspace_root();
479509
let words = shell_words::split(task)
480-
.with_context(|| format!("Error interpreting post-install task `{task}`"))?;
510+
.with_context(|| format!("Error interpreting {pre_or_post}-install task `{task}`"))?;
481511
let canonicalized = dfx_core::fs::canonicalize(&cwd.join(&words[0]))
482512
.or_else(|_| which::which(&words[0]))
483513
.map_err(|_| anyhow!("Cannot find command or file {}", &words[0]))?;
514+
484515
let mut command = Command::new(canonicalized);
485516
command.args(&words[1..]);
486517
let vars =
@@ -492,13 +523,14 @@ fn run_post_install_task(
492523
.current_dir(cwd)
493524
.stdout(Stdio::inherit())
494525
.stderr(Stdio::inherit());
526+
495527
let status = command.status()?;
496528
if !status.success() {
497529
match status.code() {
498530
Some(code) => {
499-
bail!("The post-install task `{task}` failed with exit code {code}")
531+
bail!("The {pre_or_post}-install task `{task}` failed with exit code {code}")
500532
}
501-
None => bail!("The post-install task `{task}` was terminated by a signal"),
533+
None => bail!("The {pre_or_post}-install task `{task}` was terminated by a signal"),
502534
}
503535
}
504536
Ok(())

0 commit comments

Comments
 (0)