1use 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#[derive(clap::Args, Debug, Clone, Default)]
26pub struct ProtocolBuildConfigArgs {
27 #[arg(long, global = true)]
33 pub allow_view_function: Option<bool>,
34 #[arg(skip)]
39 pub max_move_package_size: Option<u64>,
40}
41
42impl ProtocolBuildConfigArgs {
43 pub fn fill_unset_from(&mut self, defaults: &ProtocolBuildConfig) {
47 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
58impl From<ProtocolBuildConfigArgs> for ProtocolBuildConfig {
61 fn from(args: ProtocolBuildConfigArgs) -> Self {
62 let defaults = ProtocolBuildConfig::default();
63 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#[derive(Parser)]
78#[group(id = "iota-move-build")]
79pub struct Build {
80 #[arg(long, global = true)]
83 pub with_unpublished_dependencies: bool,
84 #[arg(long, global = true)]
86 pub dump_bytecode_as_base64: bool,
87 #[arg(long, global = true, requires = "dump_bytecode_as_base64")]
92 pub ignore_chain: bool,
93 #[arg(long, global = true)]
98 pub generate_struct_layouts: bool,
99 #[arg(long, global = true)]
104 pub package_info: bool,
105 #[arg(skip)]
109 pub chain_id: Option<String>,
110 #[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 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 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 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 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 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
241fn 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 assert_eq!(size_warning(81_919, MAX), None);
270 }
271
272 #[test]
273 fn warns_between_80_percent_and_maximum() {
274 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}