iota_move_build/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5extern crate move_ir_types;
6
7use std::{
8    collections::{BTreeMap, BTreeSet, HashSet},
9    io::Write,
10    path::Path,
11    str::FromStr,
12};
13
14use fastcrypto::encoding::Base64;
15use iota_package_management::{PublishedAtError, resolve_published_id};
16use iota_types::{
17    BRIDGE_ADDRESS, IOTA_FRAMEWORK_ADDRESS, IOTA_SYSTEM_ADDRESS, MOVE_STDLIB_ADDRESS,
18    STARDUST_ADDRESS,
19    base_types::ObjectID,
20    error::{IotaError, IotaResult},
21    is_system_package,
22    move_package::{FnInfo, FnInfoKey, FnInfoMap, MovePackage},
23};
24use iota_verifier::verifier as iota_bytecode_verifier;
25use move_binary_format::{
26    CompiledModule,
27    normalized::{self, Type},
28};
29use move_bytecode_utils::{Modules, layout::SerdeLayoutBuilder, module_cache::GetModule};
30use move_compiler::{
31    compiled_unit::AnnotatedCompiledModule,
32    diagnostics::{Diagnostics, report_diagnostics_to_buffer, report_warnings},
33    editions::Edition,
34    linters::LINT_WARNING_PREFIX,
35    shared::files::MappedFiles,
36};
37use move_core_types::{
38    account_address::AccountAddress,
39    language_storage::{ModuleId, StructTag, TypeTag},
40};
41use move_package::{
42    BuildConfig as MoveBuildConfig,
43    compilation::{
44        build_plan::BuildPlan, compiled_package::CompiledPackage as MoveCompiledPackage,
45    },
46    package_hooks::{PackageHooks, PackageIdentifier},
47    resolution::resolution_graph::ResolvedGraph,
48    source_package::parsed_manifest::{OnChainInfo, PackageName, SourceManifest},
49};
50use move_symbol_pool::Symbol;
51use serde_reflection::Registry;
52
53#[cfg(test)]
54#[path = "unit_tests/build_tests.rs"]
55mod build_tests;
56
57/// Wrapper around the core Move `CompiledPackage` with some IOTA-specific
58/// traits and info
59#[derive(Debug, Clone)]
60pub struct CompiledPackage {
61    pub package: MoveCompiledPackage,
62    /// Address the package is recorded as being published at.
63    pub published_at: Result<ObjectID, PublishedAtError>,
64    /// The dependency IDs of this package
65    pub dependency_ids: PackageDependencies,
66    /// The bytecode modules that this package depends on (both directly and
67    /// transitively), i.e. on-chain dependencies.
68    pub bytecode_deps: Vec<(PackageName, CompiledModule)>,
69}
70
71/// Wrapper around the core Move `BuildConfig` with some IOTA-specific info
72#[derive(Clone)]
73pub struct BuildConfig {
74    pub config: MoveBuildConfig,
75    /// If true, run the Move bytecode verifier on the bytecode from a
76    /// successful build
77    pub run_bytecode_verifier: bool,
78    /// If true, print build diagnostics to stderr--no printing if false
79    pub print_diags_to_stderr: bool,
80    /// The chain ID that compilation is with respect to (e.g., required to
81    /// resolve published dependency IDs from the `Move.lock`).
82    pub chain_id: Option<String>,
83}
84
85impl BuildConfig {
86    pub fn new_for_testing() -> Self {
87        move_package::package_hooks::register_package_hooks(Box::new(IotaPackageHooks));
88        let mut build_config: Self = Default::default();
89        let install_dir = tempfile::tempdir().unwrap().into_path();
90        let lock_file = install_dir.join("Move.lock");
91        build_config.config.install_dir = Some(install_dir);
92        build_config.config.lock_file = Some(lock_file);
93        build_config
94            .config
95            .lint_flag
96            .set(move_compiler::linters::LintLevel::None);
97        build_config.config.silence_warnings = true;
98        build_config
99    }
100
101    pub fn new_for_testing_replace_addresses<I, S>(dep_original_addresses: I) -> Self
102    where
103        I: IntoIterator<Item = (S, ObjectID)>,
104        S: Into<String>,
105    {
106        let mut build_config = Self::new_for_testing();
107        for (addr_name, obj_id) in dep_original_addresses {
108            build_config
109                .config
110                .additional_named_addresses
111                .insert(addr_name.into(), AccountAddress::from(obj_id));
112        }
113        build_config
114    }
115
116    fn fn_info(units: &[AnnotatedCompiledModule]) -> FnInfoMap {
117        let mut fn_info_map = BTreeMap::new();
118        for u in units {
119            let mod_addr = u.named_module.address.into_inner();
120            let mod_is_test = u.attributes.is_test_or_test_only();
121            for (_, s, info) in &u.function_infos {
122                let fn_name = s.as_str().to_string();
123                let is_test = mod_is_test || info.attributes.is_test_or_test_only();
124                fn_info_map.insert(FnInfoKey { fn_name, mod_addr }, FnInfo { is_test });
125            }
126        }
127
128        fn_info_map
129    }
130
131    fn compile_package<W: Write>(
132        resolution_graph: ResolvedGraph,
133        writer: &mut W,
134    ) -> anyhow::Result<(MoveCompiledPackage, FnInfoMap)> {
135        let build_plan = BuildPlan::create(resolution_graph)?;
136        let mut fn_info = None;
137        let compiled_pkg = build_plan.compile_with_driver(writer, |compiler| {
138            let (files, units_res) = compiler.build()?;
139            match units_res {
140                Ok((units, warning_diags)) => {
141                    decorate_warnings(warning_diags, Some(&files));
142                    fn_info = Some(Self::fn_info(&units));
143                    Ok((files, units))
144                }
145                Err(error_diags) => {
146                    // with errors present don't even try decorating warnings output to avoid
147                    // clutter
148                    assert!(!error_diags.is_empty());
149                    let diags_buf =
150                        report_diagnostics_to_buffer(&files, error_diags, /* color */ true);
151                    if let Err(err) = std::io::stderr().write_all(&diags_buf) {
152                        anyhow::bail!("Cannot output compiler diagnostics: {}", err);
153                    }
154                    anyhow::bail!("Compilation error");
155                }
156            }
157        })?;
158        Ok((compiled_pkg, fn_info.unwrap()))
159    }
160
161    /// Given a `path` and a `build_config`, build the package in that path,
162    /// including its dependencies. If we are building the IOTA framework,
163    /// we skip the check that the addresses should be 0
164    pub fn build(self, path: &Path) -> IotaResult<CompiledPackage> {
165        let print_diags_to_stderr = self.print_diags_to_stderr;
166        let run_bytecode_verifier = self.run_bytecode_verifier;
167        let chain_id = self.chain_id.clone();
168        let resolution_graph = self.resolution_graph(path, chain_id.clone())?;
169        build_from_resolution_graph(
170            resolution_graph,
171            run_bytecode_verifier,
172            print_diags_to_stderr,
173            chain_id,
174        )
175    }
176
177    pub fn resolution_graph(
178        mut self,
179        path: &Path,
180        chain_id: Option<String>,
181    ) -> IotaResult<ResolvedGraph> {
182        if let Some(err_msg) = set_iota_flavor(&mut self.config) {
183            return Err(IotaError::ModuleBuildFailure { error: err_msg });
184        }
185
186        if self.print_diags_to_stderr {
187            self.config
188                .resolution_graph_for_package(path, chain_id, &mut std::io::stderr())
189        } else {
190            self.config
191                .resolution_graph_for_package(path, chain_id, &mut std::io::sink())
192        }
193        .map_err(|err| IotaError::ModuleBuildFailure {
194            error: format!("{:?}", err),
195        })
196    }
197}
198
199/// There may be additional information that needs to be displayed after
200/// diagnostics are reported (optionally report diagnostics themselves if files
201/// argument is provided).
202pub fn decorate_warnings(warning_diags: Diagnostics, files: Option<&MappedFiles>) {
203    let any_linter_warnings = warning_diags.any_with_prefix(LINT_WARNING_PREFIX);
204    let (filtered_diags_num, unique) =
205        warning_diags.filtered_source_diags_with_prefix(LINT_WARNING_PREFIX);
206    if let Some(f) = files {
207        report_warnings(f, warning_diags);
208    }
209    if any_linter_warnings {
210        eprintln!(
211            "Please report feedback on the linter warnings at https://github.com/iotaledger/iota/issues\n"
212        );
213    }
214    if filtered_diags_num > 0 {
215        eprintln!(
216            "Total number of linter warnings suppressed: {filtered_diags_num} (unique lints: {unique})"
217        );
218    }
219}
220
221/// Sets build config's default flavor to `Flavor::Iota`. Returns error message
222/// if the flavor was previously set to something else than `Flavor::Iota`.
223pub fn set_iota_flavor(build_config: &mut MoveBuildConfig) -> Option<String> {
224    use move_compiler::editions::Flavor;
225
226    let flavor = build_config.default_flavor.get_or_insert(Flavor::Iota);
227    if flavor != &Flavor::Iota {
228        return Some(format!(
229            "The flavor of the Move compiler cannot be overridden with anything but \
230                 \"{}\", but the default override was set to: \"{flavor}\"",
231            Flavor::Iota,
232        ));
233    }
234    None
235}
236
237pub fn build_from_resolution_graph(
238    resolution_graph: ResolvedGraph,
239    run_bytecode_verifier: bool,
240    print_diags_to_stderr: bool,
241    chain_id: Option<String>,
242) -> IotaResult<CompiledPackage> {
243    let (published_at, dependency_ids) = gather_published_ids(&resolution_graph, chain_id);
244
245    // collect bytecode dependencies as these are not returned as part of core
246    // `CompiledPackage`
247    let mut bytecode_deps = vec![];
248    for (name, pkg) in resolution_graph.package_table.iter() {
249        if !pkg
250            .get_sources(&resolution_graph.build_options)
251            .unwrap()
252            .is_empty()
253        {
254            continue;
255        }
256        let modules =
257            pkg.get_bytecodes_bytes()
258                .map_err(|error| IotaError::ModuleDeserializationFailure {
259                    error: format!(
260                        "Deserializing bytecode dependency for package {}: {:?}",
261                        name, error
262                    ),
263                })?;
264        for module in modules {
265            let module =
266                CompiledModule::deserialize_with_defaults(module.as_ref()).map_err(|error| {
267                    IotaError::ModuleDeserializationFailure {
268                        error: format!(
269                            "Deserializing bytecode dependency for package {}: {:?}",
270                            name, error
271                        ),
272                    }
273                })?;
274            bytecode_deps.push((*name, module));
275        }
276    }
277
278    let result = if print_diags_to_stderr {
279        BuildConfig::compile_package(resolution_graph, &mut std::io::stderr())
280    } else {
281        BuildConfig::compile_package(resolution_graph, &mut std::io::sink())
282    };
283    // write build failure diagnostics to stderr, convert `error` to `String` using
284    // `Debug` format to include anyhow's error context chain.
285    let (package, fn_info) = match result {
286        Err(error) => {
287            return Err(IotaError::ModuleBuildFailure {
288                error: format!("{:?}", error),
289            });
290        }
291        Ok((package, fn_info)) => (package, fn_info),
292    };
293    let compiled_modules = package.root_modules_map();
294    if run_bytecode_verifier {
295        for m in compiled_modules.iter_modules() {
296            move_bytecode_verifier::verify_module_unmetered(m).map_err(|err| {
297                IotaError::ModuleVerificationFailure {
298                    error: err.to_string(),
299                }
300            })?;
301            iota_bytecode_verifier::iota_verify_module_unmetered(m, &fn_info)?;
302        }
303        // TODO(https://github.com/iotaledger/iota/issues/69): Run Move linker
304    }
305    Ok(CompiledPackage {
306        package,
307        published_at,
308        dependency_ids,
309        bytecode_deps,
310    })
311}
312
313impl CompiledPackage {
314    /// Return all of the bytecode modules in this package (not including direct
315    /// or transitive deps) Note: these are not topologically sorted by
316    /// dependency--use `get_dependency_sorted_modules` to produce a list of
317    /// modules suitable for publishing or static analysis
318    pub fn get_modules(&self) -> impl Iterator<Item = &CompiledModule> {
319        self.package.root_modules().map(|m| &m.unit.module)
320    }
321
322    /// Return all of the bytecode modules in this package (not including direct
323    /// or transitive deps) Note: these are not topologically sorted by
324    /// dependency--use `get_dependency_sorted_modules` to produce a list of
325    /// modules suitable for publishing or static analysis
326    pub fn into_modules(self) -> Vec<CompiledModule> {
327        self.package
328            .root_compiled_units
329            .into_iter()
330            .map(|m| m.unit.module)
331            .collect()
332    }
333
334    /// Return all of the bytecode modules that this package depends on (both
335    /// directly and transitively) Note: these are not topologically sorted
336    /// by dependency.
337    pub fn get_dependent_modules(&self) -> impl Iterator<Item = &CompiledModule> {
338        self.package
339            .deps_compiled_units
340            .iter()
341            .map(|(_, m)| &m.unit.module)
342            .chain(self.bytecode_deps.iter().map(|(_, m)| m))
343    }
344
345    /// Return all of the bytecode modules in this package and the modules of
346    /// its direct and transitive dependencies. Note: these are not
347    /// topologically sorted by dependency.
348    pub fn get_modules_and_deps(&self) -> impl Iterator<Item = &CompiledModule> {
349        self.package
350            .all_modules()
351            .map(|m| &m.unit.module)
352            .chain(self.bytecode_deps.iter().map(|(_, m)| m))
353    }
354
355    /// Return the bytecode modules in this package, topologically sorted in
356    /// dependency order. Optionally include dependencies that have not been
357    /// published (are at address 0x0), if `with_unpublished_deps` is true.
358    /// This is the function to call if you would like to publish
359    /// or statically analyze the modules.
360    pub fn get_dependency_sorted_modules(
361        &self,
362        with_unpublished_deps: bool,
363    ) -> Vec<CompiledModule> {
364        let all_modules = Modules::new(self.get_modules_and_deps());
365
366        // SAFETY: package built successfully
367        let modules = all_modules.compute_topological_order().unwrap();
368
369        if with_unpublished_deps {
370            // For each transitive dependent module, if they are not to be published, they
371            // must have a non-zero address (meaning they are already published
372            // on-chain).
373            modules
374                .filter(|module| module.address() == &AccountAddress::ZERO)
375                .cloned()
376                .collect()
377        } else {
378            // Collect all module IDs from the current package to be published (module names
379            // are not sufficient as we may have modules with the same names in
380            // user code and in IOTA framework which would result in the latter
381            // being pulled into a set of modules to be published).
382            let self_modules: HashSet<_> = self
383                .package
384                .root_modules_map()
385                .iter_modules()
386                .iter()
387                .map(|m| m.self_id())
388                .collect();
389
390            modules
391                .filter(|module| self_modules.contains(&module.self_id()))
392                .cloned()
393                .collect()
394        }
395    }
396
397    /// Return the set of Object IDs corresponding to this package's transitive
398    /// dependencies' storage package IDs (where to load those packages
399    /// on-chain).
400    pub fn get_dependency_storage_package_ids(&self) -> Vec<ObjectID> {
401        self.dependency_ids.published.values().copied().collect()
402    }
403
404    pub fn get_package_digest(&self, with_unpublished_deps: bool) -> [u8; 32] {
405        MovePackage::compute_digest_for_modules_and_deps(
406            &self.get_package_bytes(with_unpublished_deps),
407            self.dependency_ids.published.values(),
408        )
409    }
410
411    /// Return a serialized representation of the bytecode modules in this
412    /// package, topologically sorted in dependency order
413    pub fn get_package_bytes(&self, with_unpublished_deps: bool) -> Vec<Vec<u8>> {
414        self.get_dependency_sorted_modules(with_unpublished_deps)
415            .iter()
416            .map(|m| {
417                let mut bytes = Vec::new();
418                m.serialize_with_version(m.version, &mut bytes).unwrap(); // safe because package built successfully
419                bytes
420            })
421            .collect()
422    }
423
424    /// Return the base64-encoded representation of the bytecode modules in this
425    /// package, topologically sorted in dependency order
426    pub fn get_package_base64(&self, with_unpublished_deps: bool) -> Vec<Base64> {
427        self.get_package_bytes(with_unpublished_deps)
428            .iter()
429            .map(|b| Base64::from_bytes(b))
430            .collect()
431    }
432
433    /// Get bytecode modules from Bridge that are used by this package
434    pub fn get_bridge_modules(&self) -> impl Iterator<Item = &CompiledModule> {
435        self.get_modules_and_deps()
436            .filter(|m| *m.self_id().address() == BRIDGE_ADDRESS)
437    }
438
439    /// Get bytecode modules from the IOTA System that are used by this package
440    pub fn get_iota_system_modules(&self) -> impl Iterator<Item = &CompiledModule> {
441        self.get_modules_and_deps()
442            .filter(|m| *m.self_id().address() == IOTA_SYSTEM_ADDRESS)
443    }
444
445    /// Get bytecode modules from the IOTA Framework that are used by this
446    /// package
447    pub fn get_iota_framework_modules(&self) -> impl Iterator<Item = &CompiledModule> {
448        self.get_modules_and_deps()
449            .filter(|m| *m.self_id().address() == IOTA_FRAMEWORK_ADDRESS)
450    }
451
452    /// Get bytecode modules from the Move stdlib that are used by this package
453    pub fn get_stdlib_modules(&self) -> impl Iterator<Item = &CompiledModule> {
454        self.get_modules_and_deps()
455            .filter(|m| *m.self_id().address() == MOVE_STDLIB_ADDRESS)
456    }
457
458    /// Get bytecode modules from Stardust that are used by this package
459    pub fn get_stardust_modules(&self) -> impl Iterator<Item = &CompiledModule> {
460        self.get_modules_and_deps()
461            .filter(|m| *m.self_id().address() == STARDUST_ADDRESS)
462    }
463
464    /// Generate layout schemas for all types declared by this package, as well
465    /// as all struct types passed into `entry` functions declared by
466    /// modules in this package (either directly or by reference).
467    /// These layout schemas can be consumed by clients (e.g., the TypeScript
468    /// SDK) to enable BCS serialization/deserialization of the package's
469    /// objects, tx arguments, and events.
470    pub fn generate_struct_layouts(&self) -> Registry {
471        let mut package_types = BTreeSet::new();
472        for m in self.get_modules() {
473            let normalized_m = normalized::Module::new(m);
474            // 1. generate struct layouts for all declared types
475            'structs: for (name, s) in normalized_m.structs {
476                let mut dummy_type_parameters = Vec::new();
477                for t in &s.type_parameters {
478                    if t.is_phantom {
479                        // if all of t's type parameters are phantom, we can generate a type layout
480                        // we make this happen by creating a StructTag with dummy `type_params`,
481                        // since the layout generator won't look at them. we
482                        // need to do this because SerdeLayoutBuilder will refuse to generate a
483                        // layout for any open StructTag, but phantom types
484                        // cannot affect the layout of a struct, so we just use dummy values
485                        dummy_type_parameters.push(TypeTag::Signer)
486                    } else {
487                        // open type--do not attempt to generate a layout
488                        // TODO: handle generating layouts for open types?
489                        continue 'structs;
490                    }
491                }
492                debug_assert!(dummy_type_parameters.len() == s.type_parameters.len());
493                package_types.insert(StructTag {
494                    address: *m.address(),
495                    module: m.name().to_owned(),
496                    name,
497                    type_params: dummy_type_parameters,
498                });
499            }
500            // 2. generate struct layouts for all parameters of `entry` funs
501            for (_name, f) in normalized_m.functions {
502                if f.is_entry {
503                    for t in f.parameters {
504                        let tag_opt = match t.clone() {
505                            Type::Address
506                            | Type::Bool
507                            | Type::Signer
508                            | Type::TypeParameter(_)
509                            | Type::U8
510                            | Type::U16
511                            | Type::U32
512                            | Type::U64
513                            | Type::U128
514                            | Type::U256
515                            | Type::Vector(_) => continue,
516                            Type::Reference(t) | Type::MutableReference(t) => t.into_struct_tag(),
517                            s @ Type::Struct { .. } => s.into_struct_tag(),
518                        };
519                        if let Some(tag) = tag_opt {
520                            package_types.insert(tag);
521                        }
522                    }
523                }
524            }
525        }
526        let mut layout_builder = SerdeLayoutBuilder::new(self);
527        for typ in &package_types {
528            layout_builder.build_data_layout(typ).unwrap();
529        }
530        layout_builder.into_registry()
531    }
532
533    /// Checks whether this package corresponds to a built-in framework
534    pub fn is_system_package(&self) -> bool {
535        // System packages always have "published-at" addresses
536        let Ok(published_at) = self.published_at else {
537            return false;
538        };
539
540        is_system_package(published_at)
541    }
542
543    /// Checks for root modules with non-zero package addresses.  Returns an
544    /// arbitrary one, if one can be found, otherwise returns `None`.
545    pub fn published_root_module(&self) -> Option<&CompiledModule> {
546        self.package.root_compiled_units.iter().find_map(|unit| {
547            if unit.unit.module.self_id().address() != &AccountAddress::ZERO {
548                Some(&unit.unit.module)
549            } else {
550                None
551            }
552        })
553    }
554
555    pub fn verify_unpublished_dependencies(
556        &self,
557        unpublished_deps: &BTreeSet<Symbol>,
558    ) -> IotaResult<()> {
559        if unpublished_deps.is_empty() {
560            return Ok(());
561        }
562
563        let errors = self
564            .package
565            .deps_compiled_units
566            .iter()
567            .filter_map(|(p, m)| {
568                if !unpublished_deps.contains(p) || m.unit.module.address() == &AccountAddress::ZERO
569                {
570                    return None;
571                }
572                Some(format!(
573                    " - {}::{} in dependency {}",
574                    m.unit.module.address(),
575                    m.unit.name,
576                    p
577                ))
578            })
579            .collect::<Vec<String>>();
580
581        if errors.is_empty() {
582            return Ok(());
583        }
584
585        let mut error_message = vec![];
586        error_message.push(
587            "The following modules in package dependencies set a non-zero self-address:".into(),
588        );
589        error_message.extend(errors);
590        error_message.push(
591            "If these packages really are unpublished, their self-addresses should be set \
592	     to \"0x0\" in the [addresses] section of the manifest when publishing. If they \
593	     are already published, ensure they specify the address in the `published-at` of \
594	     their Move.toml manifest."
595                .into(),
596        );
597
598        Err(IotaError::ModulePublishFailure {
599            error: error_message.join("\n"),
600        })
601    }
602}
603
604impl Default for BuildConfig {
605    fn default() -> Self {
606        let config = MoveBuildConfig {
607            default_flavor: Some(move_compiler::editions::Flavor::Iota),
608            ..MoveBuildConfig::default()
609        };
610        BuildConfig {
611            config,
612            run_bytecode_verifier: true,
613            print_diags_to_stderr: false,
614            chain_id: None,
615        }
616    }
617}
618
619impl GetModule for CompiledPackage {
620    type Error = anyhow::Error;
621    // TODO: return ref here for better efficiency? Borrow checker +
622    // all_modules_map() make it hard to do this
623    type Item = CompiledModule;
624
625    fn get_module_by_id(&self, id: &ModuleId) -> Result<Option<Self::Item>, Self::Error> {
626        Ok(self.package.all_modules_map().get_module(id).ok().cloned())
627    }
628}
629
630pub const PUBLISHED_AT_MANIFEST_FIELD: &str = "published-at";
631
632pub struct IotaPackageHooks;
633
634impl PackageHooks for IotaPackageHooks {
635    fn custom_package_info_fields(&self) -> Vec<String> {
636        vec![
637            PUBLISHED_AT_MANIFEST_FIELD.to_string(),
638            // TODO: remove this once version fields are removed from all manifests
639            "version".to_string(),
640        ]
641    }
642
643    fn resolve_on_chain_dependency(
644        &self,
645        _dep_name: move_symbol_pool::Symbol,
646        _info: &OnChainInfo,
647    ) -> anyhow::Result<()> {
648        Ok(())
649    }
650
651    fn custom_resolve_pkg_id(
652        &self,
653        manifest: &SourceManifest,
654    ) -> anyhow::Result<PackageIdentifier> {
655        if (!cfg!(debug_assertions) || cfg!(test))
656            && manifest.package.edition == Some(Edition::DEVELOPMENT)
657        {
658            return Err(Edition::DEVELOPMENT.unknown_edition_error());
659        }
660        Ok(manifest.package.name)
661    }
662
663    fn resolve_version(&self, _: &SourceManifest) -> anyhow::Result<Option<Symbol>> {
664        Ok(None)
665    }
666}
667
668#[derive(Debug, Clone)]
669pub struct PackageDependencies {
670    /// Set of published dependencies (name and address).
671    pub published: BTreeMap<Symbol, ObjectID>,
672    /// Set of unpublished dependencies (name).
673    pub unpublished: BTreeSet<Symbol>,
674    /// Set of dependencies with invalid `published-at` addresses.
675    pub invalid: BTreeMap<Symbol, String>,
676    /// Set of dependencies that have conflicting `published-at` addresses. The
677    /// key refers to the package, and the tuple refers to the address in
678    /// the (Move.lock, Move.toml) respectively.
679    pub conflicting: BTreeMap<Symbol, (ObjectID, ObjectID)>,
680}
681
682/// Partition packages in `resolution_graph` into one of four groups:
683/// - The ID that the package itself is published at (if it is published)
684/// - The IDs of dependencies that have been published
685/// - The names of packages that have not been published on chain.
686/// - The names of packages that have a `published-at` field that isn't filled
687///   with a valid address.
688pub fn gather_published_ids(
689    resolution_graph: &ResolvedGraph,
690    chain_id: Option<String>,
691) -> (Result<ObjectID, PublishedAtError>, PackageDependencies) {
692    let root = resolution_graph.root_package();
693
694    let mut published = BTreeMap::new();
695    let mut unpublished = BTreeSet::new();
696    let mut invalid = BTreeMap::new();
697    let mut conflicting = BTreeMap::new();
698    let mut published_at = Err(PublishedAtError::NotPresent);
699
700    for (name, package) in &resolution_graph.package_table {
701        let property = resolve_published_id(package, chain_id.clone());
702        if name == &root {
703            // Separate out the root package as a special case
704            published_at = property;
705            continue;
706        }
707
708        match property {
709            Ok(id) => {
710                published.insert(*name, id);
711            }
712            Err(PublishedAtError::NotPresent) => {
713                unpublished.insert(*name);
714            }
715            Err(PublishedAtError::Invalid(value)) => {
716                invalid.insert(*name, value);
717            }
718            Err(PublishedAtError::Conflict {
719                id_lock,
720                id_manifest,
721            }) => {
722                conflicting.insert(*name, (id_lock, id_manifest));
723            }
724        };
725    }
726
727    (
728        published_at,
729        PackageDependencies {
730            published,
731            unpublished,
732            invalid,
733            conflicting,
734        },
735    )
736}
737
738pub fn published_at_property(manifest: &SourceManifest) -> Result<ObjectID, PublishedAtError> {
739    let Some(value) = manifest
740        .package
741        .custom_properties
742        .get(&Symbol::from(PUBLISHED_AT_MANIFEST_FIELD))
743    else {
744        return Err(PublishedAtError::NotPresent);
745    };
746
747    ObjectID::from_str(value.as_str()).map_err(|_| PublishedAtError::Invalid(value.to_owned()))
748}
749
750pub fn check_unpublished_dependencies(unpublished: &BTreeSet<Symbol>) -> Result<(), IotaError> {
751    if unpublished.is_empty() {
752        return Ok(());
753    };
754
755    let mut error_messages = unpublished
756        .iter()
757        .map(|name| {
758            format!(
759                "Package dependency \"{name}\" does not specify a published address \
760		 (the Move.toml manifest for \"{name}\" does not contain a 'published-at' field, nor is there a 'published-id' in the Move.lock).",
761            )
762        })
763        .collect::<Vec<_>>();
764
765    error_messages.push(
766        "If this is intentional, you may use the --with-unpublished-dependencies flag to \
767             continue publishing these dependencies as part of your package (they won't be \
768             linked against existing packages on-chain)."
769            .into(),
770    );
771
772    Err(IotaError::ModulePublishFailure {
773        error: error_messages.join("\n"),
774    })
775}
776
777pub fn check_invalid_dependencies(invalid: &BTreeMap<Symbol, String>) -> Result<(), IotaError> {
778    if invalid.is_empty() {
779        return Ok(());
780    }
781
782    let error_messages = invalid
783        .iter()
784        .map(|(name, value)| {
785            format!(
786                "Package dependency \"{name}\" does not specify a valid published \
787		 address: could not parse value \"{value}\" for 'published-at' field in Move.toml \
788                 or 'published-id' in Move.lock file."
789            )
790        })
791        .collect::<Vec<_>>();
792
793    Err(IotaError::ModulePublishFailure {
794        error: error_messages.join("\n"),
795    })
796}