Skip to content

Commit 403191e

Browse files
committed
feat(cli): add leo deploy --rename to deploy a program under a new name
Adds a `--rename <name>` flag to `leo deploy` that recompiles the primary program under a different on-chain identity, producing a genuinely distinct deployment. The parsed program scope is rewritten to the new name so every downstream pass emits the renamed program, and the primary compilation unit is renamed so build artifacts, the deploy read path, and the bytecode all share the renamed identity. Programs that import the original name are intentionally not redirected to the renamed copy. The rename is applied wherever the primary's source is (re)parsed: both the bytecode compile and the parse that builds the primary's stub. Otherwise the stub parse, re-reading a source that still declares the original name under the renamed unit, would fail the program-name check. Validation rejects, before any network interaction: - an invalid Aleo program name; - a rename onto a name already used by another unit or dependency (compared on the bare name, since artifacts are keyed that way); - a no-op rename to the program's current name (compared on the bare name, since a local primary's name is bare while the target is `.aleo`-suffixed); - `--rename` combined with `--build-tests` (tests keep their original names and would dangle against the renamed primary); - `--rename` when deploying multiple workspace members. The rename directive lives on the `LeoBuild` command struct rather than the shared `BuildOptions`, keeping the deploy-only concept out of the compiler options reused by build/run/test. A `canonicalize_program_name` helper in `leo-package` centralizes `.aleo`-suffix handling. Tests cover the rejections and an end-to-end deploy: `--rename` deploys to a devnode and a follow-up `leo query` confirms the program is on-chain under its new name.
1 parent d199034 commit 403191e

61 files changed

Lines changed: 746 additions & 16 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/compiler/src/compiler.rs

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use leo_package::{
3131
Manifest,
3232
PackageKind,
3333
ProgramData,
34+
bare_unit_name,
3435
resolve_workspace_dependency,
3536
};
3637
use leo_passes::*;
@@ -95,6 +96,11 @@ pub struct Compiler {
9596
output_directory: PathBuf,
9697
/// The name of the compilation unit (program or library).
9798
pub unit_name: Option<String>,
99+
/// An optional override that recompiles the program under a different on-chain
100+
/// name (e.g. `bar.aleo`). When set, the parsed program scope is rewritten to this
101+
/// identity and `unit_name` adopts it, so the emitted bytecode is a genuinely
102+
/// distinct deployment. Used by `leo deploy --rename`.
103+
pub rename: Option<String>,
98104
/// Options configuring compilation.
99105
compiler_options: CompilerOptions,
100106
/// State.
@@ -133,21 +139,37 @@ impl Compiler {
133139
.collect::<Vec<_>>();
134140

135141
// Use the parser to construct the abstract syntax tree (ast).
136-
let program = leo_parser::parse_program(
142+
let mut program = leo_parser::parse_program(
137143
self.state.handler.clone(),
138144
&self.state.node_builder,
139145
&source_file,
140146
&modules,
141147
self.state.network,
142148
)?;
143149

144-
// Check that the name of its program scope matches the expected name.
150+
// Capture the declared program name and span before any rewrite, so the
151+
// borrow does not outlive a potential mutation below.
145152
// Note that parsing enforces that there is exactly one program scope in a file.
146-
let program_scope = program.program_scopes.values().next().unwrap();
147-
if let Some(unit_name) = &self.unit_name {
148-
if unit_name != &program_scope.program_id.as_symbol().to_string() {
153+
let (source_name, source_span) = {
154+
let program_id = &program.program_scopes.values().next().unwrap().program_id;
155+
(program_id.as_symbol().to_string(), program_id.span())
156+
};
157+
158+
if let Some(rename) = self.rename.clone() {
159+
// `leo deploy --rename` recompiles the program under a different on-chain
160+
// identity. Rewrite the parsed program scope to the new name so that every
161+
// downstream pass (type checking, code generation) emits the renamed program,
162+
// and adopt it as the unit name. The source-scope equality check below is
163+
// intentionally skipped because the override replaces the declared name.
164+
let bare = bare_unit_name(&rename);
165+
let program_scope = program.program_scopes.values_mut().next().unwrap();
166+
program_scope.program_id.name.name = Symbol::intern(bare);
167+
self.unit_name = Some(rename);
168+
} else if let Some(unit_name) = &self.unit_name {
169+
// Check that the name of its program scope matches the expected name.
170+
if unit_name != &source_name {
149171
return Err(crate::errors::program_name_should_match_file_name(
150-
program_scope.program_id.as_symbol(),
172+
Symbol::intern(&source_name),
151173
// If this is a test, use the filename as the expected name.
152174
if self.state.is_test {
153175
format!(
@@ -157,12 +179,12 @@ impl Compiler {
157179
} else {
158180
format!("`{unit_name}` (specified in `program.json`)")
159181
},
160-
program_scope.program_id.span(),
182+
source_span,
161183
)
162184
.into());
163185
}
164186
} else {
165-
self.unit_name = Some(program_scope.program_id.as_symbol().to_string());
187+
self.unit_name = Some(source_name);
166188
}
167189

168190
self.state.ast = Ast::Program(program);
@@ -328,6 +350,7 @@ impl Compiler {
328350
},
329351
output_directory,
330352
unit_name: expected_unit_name,
353+
rename: None,
331354
compiler_options: compiler_options.unwrap_or_default(),
332355
import_stubs,
333356
statements_before_dce: 0,
@@ -1174,7 +1197,7 @@ mod tests {
11741197

11751198
use leo_ast::{NetworkName, NodeBuilder};
11761199
use leo_errors::{BufferEmitter, Handler};
1177-
use leo_span::{Symbol, create_session_if_not_set_then, file_source::InMemoryFileSource};
1200+
use leo_span::{Symbol, create_session_if_not_set_then, file_source::InMemoryFileSource, source_map::FileName};
11781201

11791202
use std::{path::PathBuf, rc::Rc};
11801203

@@ -1306,4 +1329,49 @@ mod tests {
13061329
);
13071330
});
13081331
}
1332+
1333+
/// Verifies that a `rename` override recompiles the program under the new name:
1334+
/// the emitted bytecode header uses the renamed identity and the original name
1335+
/// does not leak, even though the source declares the old name.
1336+
#[test]
1337+
fn rename_override_recompiles_under_new_name() {
1338+
create_session_if_not_set_then(|_| {
1339+
let handler = Handler::default();
1340+
let node_builder = Rc::new(NodeBuilder::default());
1341+
let mut compiler = Compiler::new(
1342+
Some("foo.aleo".into()),
1343+
false,
1344+
handler,
1345+
node_builder,
1346+
PathBuf::from("/unused"),
1347+
None,
1348+
IndexMap::new(),
1349+
NetworkName::TestnetV0,
1350+
);
1351+
// Request deployment under a different name.
1352+
compiler.rename = Some("bar.aleo".into());
1353+
1354+
let source = concat!(
1355+
"program foo.aleo {\n",
1356+
" record R {\n",
1357+
" owner: address,\n",
1358+
" x: bool,\n",
1359+
" }\n",
1360+
" fn foo() -> R {\n",
1361+
" return R { owner: self.signer, x: true };\n",
1362+
" }\n",
1363+
"}\n",
1364+
);
1365+
let filename = FileName::Custom("main.leo".into());
1366+
let modules: Vec<(&str, FileName)> = Vec::new();
1367+
let compiled = compiler
1368+
.compile(source, filename, &modules)
1369+
.unwrap_or_else(|err| panic!("compiling with rename failed: {err}"));
1370+
1371+
assert_eq!(compiler.unit_name.as_deref(), Some("bar.aleo"), "unit name should adopt the rename");
1372+
let bytecode = &compiled.primary.bytecode;
1373+
assert!(bytecode.contains("program bar.aleo;"), "expected renamed header, got:\n{bytecode}");
1374+
assert!(!bytecode.contains("program foo.aleo;"), "old name leaked into bytecode:\n{bytecode}");
1375+
});
1376+
}
13091377
}

crates/leo/src/cli/cli.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,24 @@ mod tests {
587587
assert!(result.is_err(), "leo new --workspace --library should fail parsing");
588588
}
589589

590+
#[test]
591+
#[serial]
592+
fn deploy_rename_flag_parses() {
593+
// `leo deploy --rename <name>` should populate the deploy command's `rename` field.
594+
let cli = CLI::try_parse_from(["leo", "deploy", "--rename", "renamed"])
595+
.expect("`leo deploy --rename renamed` should parse");
596+
match cli.command {
597+
Commands::Deploy { command } => assert_eq!(command.rename.as_deref(), Some("renamed")),
598+
_ => panic!("expected a deploy command"),
599+
}
600+
// The flag is optional: deploying without it leaves `rename` unset.
601+
let cli = CLI::try_parse_from(["leo", "deploy"]).expect("`leo deploy` should parse");
602+
match cli.command {
603+
Commands::Deploy { command } => assert_eq!(command.rename, None),
604+
_ => panic!("expected a deploy command"),
605+
}
606+
}
607+
590608
#[test]
591609
#[serial]
592610
fn new_inside_workspace_auto_registers() {
@@ -686,6 +704,7 @@ mod tests {
686704
command: Commands::Build {
687705
command: crate::cli::commands::LeoBuild {
688706
options: Default::default(),
707+
rename: None,
689708
env_override: crate::cli::commands::EnvOptions {
690709
network: Some(NetworkName::TestnetV0),
691710
..Default::default()
@@ -721,6 +740,7 @@ mod tests {
721740
command: Commands::Build {
722741
command: crate::cli::commands::LeoBuild {
723742
options: Default::default(),
743+
rename: None,
724744
env_override: crate::cli::commands::EnvOptions {
725745
network: Some(NetworkName::TestnetV0),
726746
..Default::default()
@@ -756,6 +776,7 @@ mod tests {
756776
command: Commands::Build {
757777
command: crate::cli::commands::LeoBuild {
758778
options: Default::default(),
779+
rename: None,
759780
env_override: crate::cli::commands::EnvOptions {
760781
network: Some(NetworkName::TestnetV0),
761782
..Default::default()
@@ -791,6 +812,7 @@ mod tests {
791812
command: Commands::Build {
792813
command: crate::cli::commands::LeoBuild {
793814
options: Default::default(),
815+
rename: None,
794816
env_override: Default::default(),
795817
},
796818
},
@@ -822,6 +844,7 @@ mod tests {
822844
command: Commands::Build {
823845
command: crate::cli::commands::LeoBuild {
824846
options: Default::default(),
847+
rename: None,
825848
env_override: crate::cli::commands::EnvOptions {
826849
network: Some(NetworkName::TestnetV0),
827850
..Default::default()
@@ -875,6 +898,7 @@ mod tests {
875898
command: Commands::Build {
876899
command: crate::cli::commands::LeoBuild {
877900
options: Default::default(),
901+
rename: None,
878902
env_override: crate::cli::commands::EnvOptions {
879903
network: Some(NetworkName::TestnetV0),
880904
..Default::default()
@@ -911,6 +935,7 @@ mod tests {
911935
command: Commands::Build {
912936
command: crate::cli::commands::LeoBuild {
913937
options: Default::default(),
938+
rename: None,
914939
env_override: crate::cli::commands::EnvOptions {
915940
network: Some(NetworkName::TestnetV0),
916941
..Default::default()
@@ -946,6 +971,7 @@ mod tests {
946971
command: Commands::Build {
947972
command: crate::cli::commands::LeoBuild {
948973
options: Default::default(),
974+
rename: None,
949975
env_override: crate::cli::commands::EnvOptions {
950976
network: Some(NetworkName::TestnetV0),
951977
..Default::default()
@@ -993,6 +1019,7 @@ mod tests {
9931019
},
9941020
extra: crate::cli::commands::ExtraOptions { yes: true, ..Default::default() },
9951021
skip: vec![],
1022+
rename: None,
9961023
build_options: Default::default(),
9971024
skip_deploy_certificate: true,
9981025
},
@@ -1040,6 +1067,7 @@ mod tests {
10401067
},
10411068
extra: crate::cli::commands::ExtraOptions { yes: true, ..Default::default() },
10421069
skip: vec![],
1070+
rename: None,
10431071
build_options: Default::default(),
10441072
skip_deploy_certificate: true,
10451073
},
@@ -1087,6 +1115,7 @@ mod tests {
10871115
},
10881116
extra: crate::cli::commands::ExtraOptions { yes: true, ..Default::default() },
10891117
skip: vec![],
1118+
rename: None,
10901119
build_options: Default::default(),
10911120
skip_deploy_certificate: true,
10921121
},
@@ -1135,6 +1164,7 @@ mod tests {
11351164
},
11361165
extra: crate::cli::commands::ExtraOptions { yes: true, ..Default::default() },
11371166
skip: vec![],
1167+
rename: None,
11381168
build_options: Default::default(),
11391169
skip_deploy_certificate: true,
11401170
},
@@ -1193,6 +1223,7 @@ mod tests {
11931223
command: Commands::Build {
11941224
command: crate::cli::commands::LeoBuild {
11951225
options: Default::default(),
1226+
rename: None,
11961227
env_override: crate::cli::commands::EnvOptions {
11971228
network: Some(NetworkName::TestnetV0),
11981229
..Default::default()

0 commit comments

Comments
 (0)