@@ -830,6 +830,197 @@ mod tests {
830830 let _ = std:: fs:: remove_dir_all ( & ws_root) ;
831831 }
832832
833+ #[ test]
834+ #[ serial]
835+ fn workspace_deploy_builds_all_members_test ( ) {
836+ // Workspace deploy should build all members before attempting deployment.
837+ // Without a network endpoint, deploy will fail after the build phase, but
838+ // we can verify that build artifacts were created for all members.
839+ let temp_dir = temp_dir ( ) ;
840+ let ws_root = test_helpers:: sample_workspace_with_workspace_deps ( & temp_dir, "deploy_builds" ) ;
841+
842+ let deploy = CLI {
843+ debug : false ,
844+ quiet : false ,
845+ json_output : None ,
846+ disable_update_check : false ,
847+ command : Commands :: Deploy {
848+ command : crate :: cli:: commands:: LeoDeploy {
849+ fee_options : Default :: default ( ) ,
850+ action : crate :: cli:: commands:: TransactionAction { print : false , broadcast : false , save : None } ,
851+ env_override : crate :: cli:: commands:: EnvOptions {
852+ network : Some ( NetworkName :: TestnetV0 ) ,
853+ private_key : Some ( "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" . to_string ( ) ) ,
854+ endpoint : Some ( "http://localhost:1" . to_string ( ) ) ,
855+ consensus_heights : Some ( vec ! [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ] ) ,
856+ ..Default :: default ( )
857+ } ,
858+ extra : crate :: cli:: commands:: ExtraOptions { yes : true , ..Default :: default ( ) } ,
859+ skip : vec ! [ ] ,
860+ build_options : Default :: default ( ) ,
861+ skip_deploy_certificate : true ,
862+ } ,
863+ } ,
864+ path : Some ( ws_root. clone ( ) ) ,
865+ home : None ,
866+ package : None ,
867+ } ;
868+
869+ create_session_if_not_set_then ( |_| {
870+ // Deploy will fail because there is no reachable endpoint, but it
871+ // should have already built both workspace members before that.
872+ let _ = run_with_args ( deploy) ;
873+ } ) ;
874+
875+ // Verify both members were built.
876+ assert ! ( ws_root. join( "token/build/main.aleo" ) . exists( ) , "token should be built" ) ;
877+ assert ! ( ws_root. join( "swap/build/main.aleo" ) . exists( ) , "swap should be built" ) ;
878+
879+ let _ = std:: fs:: remove_dir_all ( & ws_root) ;
880+ }
881+
882+ #[ test]
883+ #[ serial]
884+ fn workspace_deploy_package_flag_test ( ) {
885+ // With --package, deploy should only build and deploy that member.
886+ let temp_dir = temp_dir ( ) ;
887+ let ws_root = test_helpers:: sample_workspace_with_workspace_deps ( & temp_dir, "deploy_pkg_flag" ) ;
888+
889+ let deploy = CLI {
890+ debug : false ,
891+ quiet : false ,
892+ json_output : None ,
893+ disable_update_check : false ,
894+ command : Commands :: Deploy {
895+ command : crate :: cli:: commands:: LeoDeploy {
896+ fee_options : Default :: default ( ) ,
897+ action : crate :: cli:: commands:: TransactionAction { print : false , broadcast : false , save : None } ,
898+ env_override : crate :: cli:: commands:: EnvOptions {
899+ network : Some ( NetworkName :: TestnetV0 ) ,
900+ private_key : Some ( "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" . to_string ( ) ) ,
901+ endpoint : Some ( "http://localhost:1" . to_string ( ) ) ,
902+ consensus_heights : Some ( vec ! [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ] ) ,
903+ ..Default :: default ( )
904+ } ,
905+ extra : crate :: cli:: commands:: ExtraOptions { yes : true , ..Default :: default ( ) } ,
906+ skip : vec ! [ ] ,
907+ build_options : Default :: default ( ) ,
908+ skip_deploy_certificate : true ,
909+ } ,
910+ } ,
911+ path : Some ( ws_root. clone ( ) ) ,
912+ home : None ,
913+ // Filter to just token.
914+ package : Some ( "token" . to_string ( ) ) ,
915+ } ;
916+
917+ create_session_if_not_set_then ( |_| {
918+ let _ = run_with_args ( deploy) ;
919+ } ) ;
920+
921+ // --package=token targets a single member, so resolve_targets returns 1 target.
922+ // This falls through to the single-package deploy path, building only token.
923+ assert ! ( ws_root. join( "token/build/main.aleo" ) . exists( ) , "token should be built" ) ;
924+ assert ! ( !ws_root. join( "swap/build/main.aleo" ) . exists( ) , "swap should NOT be built" ) ;
925+
926+ let _ = std:: fs:: remove_dir_all ( & ws_root) ;
927+ }
928+
929+ #[ test]
930+ #[ serial]
931+ fn workspace_deploy_all_libraries_error_test ( ) {
932+ // Deploying a workspace where every member is a library should error.
933+ let temp_dir = temp_dir ( ) ;
934+ let ws_root = test_helpers:: sample_workspace_all_libraries ( & temp_dir, "deploy_all_libs" ) ;
935+
936+ let deploy = CLI {
937+ debug : false ,
938+ quiet : false ,
939+ json_output : None ,
940+ disable_update_check : false ,
941+ command : Commands :: Deploy {
942+ command : crate :: cli:: commands:: LeoDeploy {
943+ fee_options : Default :: default ( ) ,
944+ action : crate :: cli:: commands:: TransactionAction { print : false , broadcast : false , save : None } ,
945+ env_override : crate :: cli:: commands:: EnvOptions {
946+ network : Some ( NetworkName :: TestnetV0 ) ,
947+ private_key : Some ( "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" . to_string ( ) ) ,
948+ endpoint : Some ( "http://localhost:1" . to_string ( ) ) ,
949+ consensus_heights : Some ( vec ! [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ] ) ,
950+ ..Default :: default ( )
951+ } ,
952+ extra : crate :: cli:: commands:: ExtraOptions { yes : true , ..Default :: default ( ) } ,
953+ skip : vec ! [ ] ,
954+ build_options : Default :: default ( ) ,
955+ skip_deploy_certificate : true ,
956+ } ,
957+ } ,
958+ path : Some ( ws_root. clone ( ) ) ,
959+ home : None ,
960+ package : None ,
961+ } ;
962+
963+ create_session_if_not_set_then ( |_| {
964+ let result = run_with_args ( deploy) ;
965+ assert ! ( result. is_err( ) , "deploy of all-library workspace should fail" ) ;
966+ let err = result. unwrap_err ( ) . to_string ( ) ;
967+ assert ! (
968+ err. contains( "No deployable workspace members found" ) ,
969+ "expected 'No deployable workspace members found', got: {err}"
970+ ) ;
971+ } ) ;
972+
973+ let _ = std:: fs:: remove_dir_all ( & ws_root) ;
974+ }
975+
976+ #[ test]
977+ #[ serial]
978+ fn workspace_deploy_mixed_library_program_test ( ) {
979+ // Workspace with one library and one program member. Only the program
980+ // member should be deployed (library is skipped).
981+ let temp_dir = temp_dir ( ) ;
982+ let ws_root = test_helpers:: sample_workspace_mixed ( & temp_dir, "deploy_mixed" ) ;
983+
984+ let deploy = CLI {
985+ debug : false ,
986+ quiet : false ,
987+ json_output : None ,
988+ disable_update_check : false ,
989+ command : Commands :: Deploy {
990+ command : crate :: cli:: commands:: LeoDeploy {
991+ fee_options : Default :: default ( ) ,
992+ action : crate :: cli:: commands:: TransactionAction { print : false , broadcast : false , save : None } ,
993+ env_override : crate :: cli:: commands:: EnvOptions {
994+ network : Some ( NetworkName :: TestnetV0 ) ,
995+ private_key : Some ( "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" . to_string ( ) ) ,
996+ endpoint : Some ( "http://localhost:1" . to_string ( ) ) ,
997+ consensus_heights : Some ( vec ! [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ] ) ,
998+ ..Default :: default ( )
999+ } ,
1000+ extra : crate :: cli:: commands:: ExtraOptions { yes : true , ..Default :: default ( ) } ,
1001+ skip : vec ! [ ] ,
1002+ build_options : Default :: default ( ) ,
1003+ skip_deploy_certificate : true ,
1004+ } ,
1005+ } ,
1006+ path : Some ( ws_root. clone ( ) ) ,
1007+ home : None ,
1008+ package : None ,
1009+ } ;
1010+
1011+ create_session_if_not_set_then ( |_| {
1012+ // Deploy will fail at network, but build phase should succeed.
1013+ let _ = run_with_args ( deploy) ;
1014+ } ) ;
1015+
1016+ // The app program should be built.
1017+ assert ! ( ws_root. join( "app/build/main.aleo" ) . exists( ) , "app should be built" ) ;
1018+ // utils is a library - no main.aleo output.
1019+ assert ! ( !ws_root. join( "utils/build/main.aleo" ) . exists( ) , "utils library should not produce main.aleo" ) ;
1020+
1021+ let _ = std:: fs:: remove_dir_all ( & ws_root) ;
1022+ }
1023+
8331024 #[ test]
8341025 #[ serial]
8351026 fn workspace_backward_compat_test ( ) {
@@ -1172,6 +1363,151 @@ program swap.aleo {
11721363 ws_root
11731364 }
11741365
1366+ /// Workspace where every member is a library (no `.aleo` programs).
1367+ pub ( crate ) fn sample_workspace_all_libraries ( temp_dir : & Path , name : & str ) -> PathBuf {
1368+ let ws_root = temp_dir. join ( format ! ( "ws_{name}" ) ) ;
1369+
1370+ if ws_root. exists ( ) {
1371+ std:: fs:: remove_dir_all ( & ws_root) . unwrap ( ) ;
1372+ }
1373+ std:: fs:: create_dir_all ( & ws_root) . unwrap ( ) ;
1374+
1375+ let lib_a = ws_root. join ( "lib_a" ) ;
1376+ let lib_b = ws_root. join ( "lib_b" ) ;
1377+
1378+ // Create lib_a.
1379+ std:: fs:: create_dir_all ( lib_a. join ( "src" ) ) . unwrap ( ) ;
1380+ std:: fs:: write ( lib_a. join ( "src/lib.leo" ) , "" ) . unwrap ( ) ;
1381+ std:: fs:: write (
1382+ lib_a. join ( leo_package:: MANIFEST_FILENAME ) ,
1383+ serde_json:: to_string_pretty ( & serde_json:: json!( {
1384+ "program" : "lib_a" ,
1385+ "version" : "0.1.0" ,
1386+ "description" : "" ,
1387+ "license" : "MIT" ,
1388+ "leo" : env!( "CARGO_PKG_VERSION" ) ,
1389+ "dependencies" : null,
1390+ "dev_dependencies" : null
1391+ } ) )
1392+ . unwrap ( ) ,
1393+ )
1394+ . unwrap ( ) ;
1395+
1396+ // Create lib_b.
1397+ std:: fs:: create_dir_all ( lib_b. join ( "src" ) ) . unwrap ( ) ;
1398+ std:: fs:: write ( lib_b. join ( "src/lib.leo" ) , "" ) . unwrap ( ) ;
1399+ std:: fs:: write (
1400+ lib_b. join ( leo_package:: MANIFEST_FILENAME ) ,
1401+ serde_json:: to_string_pretty ( & serde_json:: json!( {
1402+ "program" : "lib_b" ,
1403+ "version" : "0.1.0" ,
1404+ "description" : "" ,
1405+ "license" : "MIT" ,
1406+ "leo" : env!( "CARGO_PKG_VERSION" ) ,
1407+ "dependencies" : null,
1408+ "dev_dependencies" : null
1409+ } ) )
1410+ . unwrap ( ) ,
1411+ )
1412+ . unwrap ( ) ;
1413+
1414+ // Write workspace.json.
1415+ std:: fs:: write (
1416+ ws_root. join ( leo_package:: WORKSPACE_MANIFEST_FILENAME ) ,
1417+ serde_json:: to_string_pretty ( & serde_json:: json!( {
1418+ "members" : [ "lib_a" , "lib_b" ]
1419+ } ) )
1420+ . unwrap ( ) ,
1421+ )
1422+ . unwrap ( ) ;
1423+
1424+ ws_root
1425+ }
1426+
1427+ /// Workspace with one library member (`utils`) and one program member
1428+ /// (`app`) that imports it.
1429+ pub ( crate ) fn sample_workspace_mixed ( temp_dir : & Path , name : & str ) -> PathBuf {
1430+ let ws_root = temp_dir. join ( format ! ( "ws_{name}" ) ) ;
1431+
1432+ if ws_root. exists ( ) {
1433+ std:: fs:: remove_dir_all ( & ws_root) . unwrap ( ) ;
1434+ }
1435+ std:: fs:: create_dir_all ( & ws_root) . unwrap ( ) ;
1436+
1437+ let utils_dir = ws_root. join ( "utils" ) ;
1438+ let app_dir = ws_root. join ( "app" ) ;
1439+
1440+ // Create utils library.
1441+ std:: fs:: create_dir_all ( utils_dir. join ( "src" ) ) . unwrap ( ) ;
1442+ std:: fs:: write (
1443+ utils_dir. join ( "src/lib.leo" ) ,
1444+ "\
1445+ const FACTOR: u32 = 2u32;
1446+ " ,
1447+ )
1448+ . unwrap ( ) ;
1449+ std:: fs:: write (
1450+ utils_dir. join ( leo_package:: MANIFEST_FILENAME ) ,
1451+ serde_json:: to_string_pretty ( & serde_json:: json!( {
1452+ "program" : "utils" ,
1453+ "version" : "0.1.0" ,
1454+ "description" : "" ,
1455+ "license" : "MIT" ,
1456+ "leo" : env!( "CARGO_PKG_VERSION" ) ,
1457+ "dependencies" : null,
1458+ "dev_dependencies" : null
1459+ } ) )
1460+ . unwrap ( ) ,
1461+ )
1462+ . unwrap ( ) ;
1463+
1464+ // Create app program (depends on utils via workspace).
1465+ std:: fs:: create_dir_all ( app_dir. join ( "src" ) ) . unwrap ( ) ;
1466+ std:: fs:: write (
1467+ app_dir. join ( "src/main.leo" ) ,
1468+ "\
1469+ program app.aleo {
1470+ fn run(x: u32) -> u32 {
1471+ return x * utils::FACTOR;
1472+ }
1473+
1474+ @noupgrade
1475+ constructor() {}
1476+ }
1477+ " ,
1478+ )
1479+ . unwrap ( ) ;
1480+ std:: fs:: write (
1481+ app_dir. join ( leo_package:: MANIFEST_FILENAME ) ,
1482+ serde_json:: to_string_pretty ( & serde_json:: json!( {
1483+ "program" : "app.aleo" ,
1484+ "version" : "0.1.0" ,
1485+ "description" : "" ,
1486+ "license" : "MIT" ,
1487+ "leo" : env!( "CARGO_PKG_VERSION" ) ,
1488+ "dependencies" : [ {
1489+ "name" : "utils" ,
1490+ "location" : "workspace"
1491+ } ] ,
1492+ "dev_dependencies" : null
1493+ } ) )
1494+ . unwrap ( ) ,
1495+ )
1496+ . unwrap ( ) ;
1497+
1498+ // Write workspace.json (utils first so it builds before app).
1499+ std:: fs:: write (
1500+ ws_root. join ( leo_package:: WORKSPACE_MANIFEST_FILENAME ) ,
1501+ serde_json:: to_string_pretty ( & serde_json:: json!( {
1502+ "members" : [ "utils" , "app" ]
1503+ } ) )
1504+ . unwrap ( ) ,
1505+ )
1506+ . unwrap ( ) ;
1507+
1508+ ws_root
1509+ }
1510+
11751511 pub ( crate ) fn sample_nested_package ( temp_dir : & Path ) {
11761512 let name = "nested" ;
11771513
0 commit comments