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