Skip to main content

iota_move/
build.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fs, path::Path};
6
7use clap::Parser;
8use colored::Colorize;
9use iota_move_build::{BuildConfig, CompiledPackage, ProtocolBuildConfig, implicit_deps};
10use iota_package_management::system_package_versions::latest_system_packages;
11use iota_protocol_config::ProtocolConfig;
12use move_cli::base;
13use move_package::BuildConfig as MoveBuildConfig;
14
15use crate::manage_package::resolve_lock_file_path;
16
17const LAYOUTS_DIR: &str = "layouts";
18const STRUCT_LAYOUTS_FILENAME: &str = "struct_layouts.yaml";
19
20/// CLI-flattened, per-field overrides mirroring [`ProtocolBuildConfig`].
21///
22/// This lives here rather than in `iota-types` so that `iota-types` need not
23/// depend on `clap`. Each field is an optional override: `None` keeps the value
24/// from the network-resolved [`ProtocolBuildConfig`].
25#[derive(clap::Args, Debug, Clone, Default)]
26pub struct ProtocolBuildConfigArgs {
27    /// Override whether view-function metadata is emitted and the `View`
28    /// attribute is verified. When unset, this follows the resolved protocol
29    /// config's `package_metadata_with_dynamic_module_metadata` feature; pass
30    /// `--allow-view-function true` or `--allow-view-function false` to force
31    /// it.
32    #[arg(long, global = true)]
33    pub allow_view_function: Option<bool>,
34    /// Maximum on-chain package size, populated from the resolved protocol
35    /// config rather than the command line. When set, `iota move build` reports
36    /// the package size against this real network limit; when unset (no network
37    /// resolved), it reports against the compiled-in protocol default.
38    #[arg(skip)]
39    pub max_move_package_size: Option<u64>,
40}
41
42impl ProtocolBuildConfigArgs {
43    /// Fills any override left unset on the command line with the corresponding
44    /// value from `defaults` (e.g. the target network's protocol config).
45    /// Command-line values take precedence.
46    pub fn fill_unset_from(&mut self, defaults: &ProtocolBuildConfig) {
47        // Destructured so that adding a field to `ProtocolBuildConfig` fails to
48        // compile here until the new default is wired in.
49        let ProtocolBuildConfig {
50            allow_view_function,
51            max_move_package_size,
52        } = *defaults;
53        self.allow_view_function.get_or_insert(allow_view_function);
54        self.max_move_package_size = self.max_move_package_size.or(max_move_package_size);
55    }
56}
57
58/// Resolves the CLI overrides into a [`ProtocolBuildConfig`], falling back to
59/// [`ProtocolBuildConfig::default()`] for any option left unset.
60impl From<ProtocolBuildConfigArgs> for ProtocolBuildConfig {
61    fn from(args: ProtocolBuildConfigArgs) -> Self {
62        let defaults = ProtocolBuildConfig::default();
63        // Destructured so that adding a field to `ProtocolBuildConfigArgs` fails
64        // to compile here until it is resolved.
65        let ProtocolBuildConfigArgs {
66            allow_view_function,
67            max_move_package_size,
68        } = args;
69        ProtocolBuildConfig {
70            allow_view_function: allow_view_function.unwrap_or(defaults.allow_view_function),
71            max_move_package_size: max_move_package_size.or(defaults.max_move_package_size),
72        }
73    }
74}
75
76/// Build a move package.
77#[derive(Parser)]
78#[group(id = "iota-move-build")]
79pub struct Build {
80    /// Include the contents of packages in dependencies that haven't been
81    /// published (only relevant when dumping bytecode as base64)
82    #[arg(long, global = true)]
83    pub with_unpublished_dependencies: bool,
84    /// Whether we are printing in base64.
85    #[arg(long, global = true)]
86    pub dump_bytecode_as_base64: bool,
87    /// Don't specialize the package to the active chain when dumping bytecode
88    /// as Base64. This allows building to proceed without a network connection
89    /// or active environment, but it will not be able to automatically
90    /// determine the addresses of its dependencies.
91    #[arg(long, global = true, requires = "dump_bytecode_as_base64")]
92    pub ignore_chain: bool,
93    /// If true, generate struct layout schemas for all struct types passed into
94    /// `entry` functions declared by modules in this package These layout
95    /// schemas can be consumed by clients (e.g., the TypeScript SDK) to enable
96    /// serialization/deserialization of transaction arguments and events.
97    #[arg(long, global = true)]
98    pub generate_struct_layouts: bool,
99    /// Print the package name, its direct dependencies, and the on-chain size
100    /// (against the protocol maximum). Without this flag the size is still
101    /// checked, but only a warning (near the limit) or an unpublishable notice
102    /// (over the limit) is shown.
103    #[arg(long, global = true)]
104    pub package_info: bool,
105    /// The chain ID, if resolved. Required when the dump_bytecode_as_base64 is
106    /// true, for automated address management, where package addresses are
107    /// resolved for the respective chain in the Move.lock file.
108    #[arg(skip)]
109    pub chain_id: Option<String>,
110    /// Protocol build config, either provided by the user on the command line
111    /// or populated from the target network's protocol config.
112    #[command(flatten)]
113    pub protocol_build_config_args: ProtocolBuildConfigArgs,
114}
115
116impl Build {
117    pub fn execute(
118        &self,
119        path: Option<&Path>,
120        build_config: MoveBuildConfig,
121    ) -> anyhow::Result<()> {
122        let rerooted_path = base::reroot_path(path)?;
123        let build_config = resolve_lock_file_path(build_config, Some(&rerooted_path))?;
124        Self::execute_internal(
125            &rerooted_path,
126            build_config,
127            self.generate_struct_layouts,
128            self.with_unpublished_dependencies,
129            self.package_info,
130            self.chain_id.clone(),
131            self.protocol_build_config_args.clone(),
132        )
133    }
134
135    pub fn execute_internal(
136        rerooted_path: &Path,
137        mut config: MoveBuildConfig,
138        generate_struct_layouts: bool,
139        with_unpublished_deps: bool,
140        package_info: bool,
141        chain_id: Option<String>,
142        protocol_build_config_args: ProtocolBuildConfigArgs,
143    ) -> anyhow::Result<()> {
144        config.implicit_dependencies = implicit_deps(latest_system_packages());
145        let protocol_build_config: ProtocolBuildConfig = protocol_build_config_args.into();
146        let pkg = BuildConfig {
147            config,
148            run_bytecode_verifier: true,
149            print_diags_to_stderr: true,
150            chain_id,
151            protocol_build_config,
152        }
153        .build(rerooted_path)?;
154
155        // The package size is protocol-independent, so it is always computed.
156        // The limit is protocol-gated: use the network-resolved value when a
157        // target network is known, otherwise fall back to the compiled-in
158        // default (identical across all protocol versions today).
159        let dep_count = pkg.linkage_dependency_count();
160        let size = pkg.published_size(with_unpublished_deps, dep_count);
161        let max_size = protocol_build_config
162            .max_move_package_size
163            .unwrap_or_else(|| ProtocolConfig::get_for_min_version().max_move_package_size());
164        // The near-limit / over-limit consequence is always surfaced. With
165        // `--package-info` we additionally print the full details.
166        Self::warn_on_size(size, max_size);
167        if package_info {
168            Self::print_package_info(&pkg, with_unpublished_deps, size, max_size)?;
169        }
170
171        if generate_struct_layouts {
172            let layout_str = serde_yaml::to_string(&pkg.generate_struct_layouts()).unwrap();
173            // store under <package_path>/build/<package_name>/layouts/struct_layouts.yaml
174            let dir_name = rerooted_path
175                .join("build")
176                .join(pkg.package.compiled_package_info.package_name.as_str())
177                .join(LAYOUTS_DIR);
178            let layout_filename = dir_name.join(STRUCT_LAYOUTS_FILENAME);
179            fs::create_dir_all(dir_name)?;
180            fs::write(layout_filename, layout_str)?
181        }
182
183        pkg.package
184            .compiled_package_info
185            .build_flags
186            .update_lock_file_toolchain_version(rerooted_path, env!("CARGO_PKG_VERSION").into())?;
187
188        Ok(())
189    }
190
191    /// Warn only when the package is close to or over the protocol size limit.
192    /// Under 80% of the limit nothing is printed.
193    fn warn_on_size(size: u64, max_size: u64) {
194        let Some(message) = size_warning(size, max_size) else {
195            return;
196        };
197        if size > max_size {
198            eprintln!("{}", message.red().bold());
199        } else {
200            eprintln!("{}", message.yellow());
201        }
202    }
203
204    /// Print the package name, its direct dependencies, the on-chain size, the
205    /// protocol size limit, and whether the package is publishable.
206    fn print_package_info(
207        pkg: &CompiledPackage,
208        with_unpublished_deps: bool,
209        size: u64,
210        max_size: u64,
211    ) -> anyhow::Result<()> {
212        let mut dependencies: Vec<String> = pkg
213            .find_immediate_deps_pkgs_to_keep(with_unpublished_deps)?
214            .into_keys()
215            .map(|name| name.to_string())
216            .collect();
217        dependencies.sort();
218        let dependencies = if dependencies.is_empty() {
219            "(none)".to_string()
220        } else {
221            dependencies.join(", ")
222        };
223
224        eprintln!(
225            "Package: {}",
226            pkg.package.compiled_package_info.package_name
227        );
228        eprintln!("Dependencies: {dependencies}");
229        eprintln!("Package size: {size} bytes");
230        eprintln!("Protocol maximum package size: {max_size} bytes");
231        let status = if size <= max_size {
232            "publishable".green()
233        } else {
234            "not publishable".red()
235        };
236        eprintln!("Status: {status}");
237        Ok(())
238    }
239}
240
241/// The size report line to print, or `None` when the package is below 80% of
242/// the protocol maximum. The near-limit band starts at 80% of the maximum
243/// (`size * 5 >= max_size * 4`); above the maximum the package is not
244/// publishable.
245fn size_warning(size: u64, max_size: u64) -> Option<String> {
246    if size > max_size {
247        Some(format!(
248            "The package size {size} bytes exceeds the protocol maximum package size ({max_size} bytes) and is not publishable."
249        ))
250    } else if size * 5 >= max_size * 4 {
251        Some(format!(
252            "Warning: package size {size} bytes is above 80% of the protocol maximum package size ({max_size} bytes)."
253        ))
254    } else {
255        None
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    const MAX: u64 = 102_400;
264
265    #[test]
266    fn silent_below_80_percent() {
267        assert_eq!(size_warning(0, MAX), None);
268        // Just under 80% (81_920).
269        assert_eq!(size_warning(81_919, MAX), None);
270    }
271
272    #[test]
273    fn warns_between_80_percent_and_maximum() {
274        // Exactly 80%, mid-band, and exactly at the maximum are all warnings
275        // (still publishable) and include the size and the maximum.
276        for size in [81_920, 90_000, MAX] {
277            let message = size_warning(size, MAX).expect("expected a warning");
278            assert!(message.contains("above 80%"), "{message}");
279            assert!(
280                message.contains(&size.to_string()) && message.contains("102400"),
281                "{message}"
282            );
283        }
284    }
285
286    #[test]
287    fn reports_not_publishable_above_maximum() {
288        let message = size_warning(150_000, MAX).expect("expected a message");
289        assert!(
290            message.contains("150000")
291                && message.contains("102400")
292                && message.contains("not publishable"),
293            "{message}"
294        );
295    }
296}