Skip to content

Commit d0831dc

Browse files
committed
Add --env to set build environment variables.
1 parent 1cb6979 commit d0831dc

4 files changed

Lines changed: 304 additions & 17 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ To view the commands that will be executed, without executing them, use the --pr
396396
If ommitted, wasm files are written only to the cargo target directory.
397397

398398
- `--locked` — Assert that `Cargo.lock` will remain unchanged
399+
- `--env <ENV>` — Set an environment variable for the build (repeatable), e.g. `--env NAME=VALUE`. It's set on the build process; for a verifiable build it's passed to the container and recorded as a `bldopt`, so avoid secrets there
399400
- `--optimize <OPTIMIZE>` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature
400401

401402
Default value: `true`
@@ -501,6 +502,7 @@ Deploy a wasm contract
501502
Default value: `false`
502503

503504
- `--alias <ALIAS>` — The alias that will be used to save the contract's id. Whenever used, `--alias` will always overwrite the existing contract id configuration without asking for confirmation
505+
- `--env <ENV>` — Set an environment variable for the build (repeatable), e.g. `--env NAME=VALUE`. It's set on the build process; for a verifiable build it's passed to the container and recorded as a `bldopt`, so avoid secrets there
504506
- `--optimize <OPTIMIZE>` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature
505507

506508
Default value: `true`
@@ -875,6 +877,7 @@ Install a WASM file to the ledger without creating a contract instance
875877

876878
Default value: `false`
877879

880+
- `--env <ENV>` — Set an environment variable for the build (repeatable), e.g. `--env NAME=VALUE`. It's set on the build process; for a verifiable build it's passed to the container and recorded as a `bldopt`, so avoid secrets there
878881
- `--optimize <OPTIMIZE>` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature
879882

880883
Default value: `true`
@@ -938,6 +941,7 @@ Install a WASM file to the ledger without creating a contract instance
938941

939942
Default value: `false`
940943

944+
- `--env <ENV>` — Set an environment variable for the build (repeatable), e.g. `--env NAME=VALUE`. It's set on the build process; for a verifiable build it's passed to the container and recorded as a `bldopt`, so avoid secrets there
941945
- `--optimize <OPTIMIZE>` — Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature
942946

943947
Default value: `true`

cmd/crates/soroban-test/tests/it/build.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,47 @@ fn build_package_by_current_dir() {
6969
));
7070
}
7171

72+
// `--env` is repeatable and sets env vars on the local cargo process; they
73+
// surface in the printed command in --print-commands-only.
74+
#[test]
75+
fn build_with_env_vars() {
76+
let sandbox = TestEnv::default();
77+
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
78+
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");
79+
sandbox
80+
.new_assert_cmd("contract")
81+
.current_dir(fixture_path)
82+
.arg("build")
83+
.arg("--print-commands-only")
84+
.arg("--env")
85+
.arg("FOO=bar")
86+
.arg("--env")
87+
.arg("BAZ=qux")
88+
.assert()
89+
.success()
90+
.stdout(predicate::str::contains("FOO=bar").and(predicate::str::contains("BAZ=qux")));
91+
}
92+
93+
// An invalid `--env` name is rejected before building.
94+
#[test]
95+
fn build_rejects_invalid_env_name() {
96+
let sandbox = TestEnv::default();
97+
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
98+
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");
99+
sandbox
100+
.new_assert_cmd("contract")
101+
.current_dir(fixture_path)
102+
.arg("build")
103+
.arg("--print-commands-only")
104+
.arg("--env")
105+
.arg("1FOO=bar")
106+
.assert()
107+
.failure()
108+
.stderr(predicate::str::contains(
109+
"not a valid environment variable name",
110+
));
111+
}
112+
72113
#[test]
73114
fn build_with_locked() {
74115
let sandbox = TestEnv::default();

cmd/soroban-cli/src/commands/contract/build.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ pub struct BuildArgs {
146146
#[arg(long, num_args=1, value_parser=parse_meta_arg, action=clap::ArgAction::Append, help_heading = "Metadata")]
147147
pub meta: Vec<(String, String)>,
148148

149+
/// Set an environment variable for the build (repeatable), e.g.
150+
/// `--env NAME=VALUE`. It's set on the build process; for a verifiable build
151+
/// it's passed to the container and recorded as a `bldopt`, so avoid secrets
152+
/// there.
153+
#[arg(
154+
long = "env",
155+
num_args = 1,
156+
value_parser = parse_env_arg,
157+
action = clap::ArgAction::Append
158+
)]
159+
pub env: Vec<(String, String)>,
160+
149161
/// Optimize the generated wasm. Enabled by default; pass `--optimize=false` to disable. Requires the `additional-libs` feature.
150162
#[arg(
151163
long,
@@ -163,6 +175,7 @@ impl Default for BuildArgs {
163175
fn default() -> Self {
164176
Self {
165177
meta: Vec::new(),
178+
env: Vec::new(),
166179
optimize: true,
167180
}
168181
}
@@ -179,6 +192,35 @@ pub fn parse_meta_arg(s: &str) -> Result<(String, String), Error> {
179192
Ok((key.to_string(), value.to_string()))
180193
}
181194

195+
/// Parse a `--env NAME=VALUE` argument. The name must be a valid environment
196+
/// variable name (`[A-Za-z_][A-Za-z0-9_]*`, no surrounding whitespace); the
197+
/// value is kept verbatim, since the shell has already resolved any quoting and
198+
/// env values can carry significant whitespace.
199+
pub fn parse_env_arg(s: &str) -> Result<(String, String), Error> {
200+
let (name, value) = s
201+
.split_once('=')
202+
.ok_or_else(|| Error::EnvArg(format!("{s:?} must be in the form 'NAME=VALUE'")))?;
203+
204+
if !is_valid_env_name(name) {
205+
return Err(Error::EnvArg(format!(
206+
"{name:?} is not a valid environment variable name (expected [A-Za-z_][A-Za-z0-9_]*)"
207+
)));
208+
}
209+
210+
Ok((name.to_string(), value.to_string()))
211+
}
212+
213+
/// Whether `name` is a valid environment variable name: a leading letter or
214+
/// underscore followed by letters, digits, or underscores.
215+
fn is_valid_env_name(name: &str) -> bool {
216+
let mut chars = name.chars();
217+
match chars.next() {
218+
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
219+
_ => return false,
220+
}
221+
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
222+
}
223+
182224
#[derive(thiserror::Error, Debug)]
183225
pub enum Error {
184226
#[error(transparent)]
@@ -220,6 +262,9 @@ pub enum Error {
220262
#[error("invalid meta entry: {0}")]
221263
MetaArg(String),
222264

265+
#[error("invalid env entry: {0}")]
266+
EnvArg(String),
267+
223268
#[error(
224269
"use a rust version other than 1.81, 1.82, 1.83 or 1.91.0 to build contracts (got {0})"
225270
)]
@@ -348,6 +393,11 @@ impl Cmd {
348393
// optimization using markers.
349394
cmd.env("SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2", "1");
350395

396+
// User-supplied build env vars (--env NAME=VALUE).
397+
for (name, value) in &self.build_args.env {
398+
cmd.env(name, value);
399+
}
400+
351401
let cmd_str = serialize_command(&cmd);
352402

353403
if self.print_commands_only {
@@ -911,4 +961,49 @@ mod tests {
911961
"shlex round-trip failed: {raw_arg:?} not found as a single token in {tokens:?}"
912962
);
913963
}
964+
965+
#[test]
966+
fn parse_env_arg_parses_name_value() {
967+
assert_eq!(
968+
parse_env_arg("FOO=bar").unwrap(),
969+
("FOO".to_string(), "bar".to_string())
970+
);
971+
assert_eq!(
972+
parse_env_arg("_FOO_BAR2=bar").unwrap(),
973+
("_FOO_BAR2".to_string(), "bar".to_string())
974+
);
975+
// Only the first `=` splits; the value keeps the rest verbatim.
976+
assert_eq!(
977+
parse_env_arg("FOO=a=b=c").unwrap(),
978+
("FOO".to_string(), "a=b=c".to_string())
979+
);
980+
// An empty value is allowed.
981+
assert_eq!(
982+
parse_env_arg("FOO=").unwrap(),
983+
("FOO".to_string(), String::new())
984+
);
985+
// The value is kept verbatim (the shell already handled quoting), so
986+
// significant whitespace survives.
987+
assert_eq!(
988+
parse_env_arg("FOO= 1 ").unwrap(),
989+
("FOO".to_string(), " 1 ".to_string())
990+
);
991+
}
992+
993+
#[test]
994+
fn parse_env_arg_rejects_invalid() {
995+
for bad in [
996+
"FOO", // no `=`
997+
"=bar", // empty name
998+
" FOO = 1 ", // whitespace in name
999+
"1FOO=x", // leading digit
1000+
"FO-O=x", // invalid char
1001+
"FOO BAR=x", // space in name
1002+
] {
1003+
assert!(
1004+
matches!(parse_env_arg(bad).unwrap_err(), Error::EnvArg(_)),
1005+
"expected {bad:?} to be rejected"
1006+
);
1007+
}
1008+
}
9141009
}

0 commit comments

Comments
 (0)