Skip to main content

daml_util/
package.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2
3use daml_grpc::DamlGrpcClient;
4use daml_grpc::data::package::DamlPackage;
5use daml_grpc::data::{DamlError, DamlResult};
6use daml_lf::element::{
7    DamlAbsoluteTyCon, DamlElementVisitor, DamlNonLocalTyCon, DamlNonLocalValueName, DamlPackage as DamlLfPackage,
8    DamlVisitableElement,
9};
10use daml_lf::{DamlLfArchive, DamlLfArchivePayload, DamlLfHashFunction, DarFile, DarManifest};
11use futures::StreamExt;
12use futures::stream::FuturesUnordered;
13use uuid::Uuid;
14
15/// Convenience methods for working with a collection of [`DamlPackage`].
16///
17/// In the following example a [`DamlPackages`] is created from all known [`DamlPackage`] on a Daml ledger and then
18/// converted into [`DarFile`] using the [`ArchiveAutoNamingStyle::Uuid`] naming style:
19///
20/// ```no_run
21/// # use daml_lf::DarFile;
22/// # use daml_grpc::DamlGrpcClientBuilder;
23/// # use std::thread;
24/// # use daml_util::package::{DamlPackages, ArchiveAutoNamingStyle};
25/// # fn main() {
26/// # futures::executor::block_on(async {
27/// let ledger_client = DamlGrpcClientBuilder::uri("http://127.0.0.1").connect().await.unwrap();
28/// let packages = DamlPackages::from_ledger(&ledger_client).await.unwrap();
29/// let dar = packages.into_dar(None, false, ArchiveAutoNamingStyle::Uuid).unwrap();
30/// # })
31/// # }
32/// ```
33#[derive(Debug)]
34pub struct DamlPackages {
35    packages: Vec<DamlPackage>,
36}
37
38impl DamlPackages {
39    pub fn new(packages: Vec<DamlPackage>) -> Self {
40        Self {
41            packages,
42        }
43    }
44
45    /// Create a [`DamlPackages`] from all known [`DamlPackage`] on a Daml ledger.
46    pub async fn from_ledger(ledger_client: &DamlGrpcClient) -> DamlResult<Self> {
47        let packages = ledger_client.package_service().list_packages().await?;
48        let handles = packages
49            .iter()
50            .map(|pd| async move { ledger_client.package_service().get_package(pd).await })
51            .collect::<FuturesUnordered<_>>();
52        let all_packages =
53            handles.collect::<Vec<DamlResult<_>>>().await.into_iter().collect::<DamlResult<Vec<DamlPackage>>>()?;
54        Ok(Self::new(all_packages))
55    }
56
57    /// Return the hash of the [`DamlPackage`] which contains a given module or en error if no such package exists.
58    ///
59    /// The supplied `module_name` name is assumed to be in `DottedName` format, i.e. `TopModule.SubModule.Module`.
60    pub fn find_module(self, module_name: &str) -> DamlResult<String> {
61        self.into_payloads()?
62            .iter()
63            .find(|(_, payload)| payload.contains_module(module_name))
64            .map_or_else(|| Err("package could not be found".into()), |(package_id, _)| Ok((*package_id).clone()))
65    }
66
67    /// Package all contained [`DamlPackage`] into a single [`DarFile`].
68    ///
69    /// * `main_package_id` — the id of the package to place as the DAR
70    ///   main. `None` picks an arbitrary package from the set (whichever
71    ///   the underlying `Vec` yields first). `Some(id)` errors out if
72    ///   no contained package matches.
73    /// * `filter_deps` — when `true`, walks the LF2 tree of the main
74    ///   package to collect its (transitive) cross-package references
75    ///   and keeps only those in the resulting DAR. When `false`, every
76    ///   contained package becomes a dependency of the main, whether or
77    ///   not it is actually reachable.
78    /// * `auto_naming_style` — how to name each contained archive.
79    pub fn into_dar(
80        self,
81        main_package_id: Option<&str>,
82        filter_deps: bool,
83        auto_naming_style: ArchiveAutoNamingStyle,
84    ) -> DamlResult<DarFile> {
85        let main_id = self.resolve_main_id(main_package_id)?;
86        let keep = if filter_deps {
87            let payloads: HashMap<String, DamlLfArchivePayload> = self.payloads_by_id()?;
88            let reachable = Self::reachable_from(&main_id, &payloads)?;
89            self.packages.into_iter().filter(|p| reachable.contains(p.hash())).collect()
90        } else {
91            self.packages
92        };
93        let all_archives = Self::packages_to_archives(keep, auto_naming_style)?;
94        Self::archives_to_dar(all_archives, &main_id)
95    }
96
97    /// Convert all contained [`DamlPackage`] into [`DamlLfArchive`].
98    ///
99    /// Note that the created archive is not named.
100    pub fn into_archives(self, auto_naming_style: ArchiveAutoNamingStyle) -> DamlResult<Vec<DamlLfArchive>> {
101        Self::packages_to_archives(self.packages, auto_naming_style)
102    }
103
104    fn packages_to_archives(
105        packages: Vec<DamlPackage>,
106        auto_naming_style: ArchiveAutoNamingStyle,
107    ) -> DamlResult<Vec<DamlLfArchive>> {
108        packages
109            .into_iter()
110            .map(|p| {
111                let hash = p.hash().to_owned();
112                let payload = Self::package_into_payload(p)?;
113                let name = match auto_naming_style {
114                    ArchiveAutoNamingStyle::Empty => String::default(),
115                    ArchiveAutoNamingStyle::Hash => hash.clone(),
116                    ArchiveAutoNamingStyle::Uuid => Uuid::new_v4().to_string(),
117                };
118                Ok(DamlLfArchive::new(name, payload, DamlLfHashFunction::Sha256, hash))
119            })
120            .collect()
121    }
122
123    /// Convert all contained [`DamlPackage`] into [`DamlLfArchivePayload`].
124    pub fn into_payloads(self) -> DamlResult<Vec<(String, DamlLfArchivePayload)>> {
125        self.packages
126            .into_iter()
127            .map(|p| {
128                let hash = p.hash().to_owned();
129                Self::package_into_payload(p).map(|pl| (hash, pl))
130            })
131            .collect::<DamlResult<Vec<_>>>()
132    }
133
134    fn package_into_payload(package: DamlPackage) -> DamlResult<DamlLfArchivePayload> {
135        DamlLfArchivePayload::from_bytes(package.take_payload()).map_err(|e| DamlError::Other(e.to_string()))
136    }
137
138    fn archives_to_dar(mut all_packages: Vec<DamlLfArchive>, main_id: &str) -> DamlResult<DarFile> {
139        if all_packages.is_empty() {
140            return Err("expected at least one archive".into());
141        }
142        let main_idx = all_packages
143            .iter()
144            .position(|a| a.hash == main_id)
145            .ok_or_else(|| DamlError::Other(format!("main package {main_id} not present in archive set")))?;
146        let first = all_packages.swap_remove(main_idx);
147        let rest = all_packages;
148        let manifest = DarManifest::new_implied(first.name.clone(), rest.iter().map(|n| n.name.clone()).collect());
149        Ok(DarFile::new(manifest, first, rest))
150    }
151
152    /// Resolve the caller-supplied main-id hint. `None` picks whatever
153    /// the underlying `Vec` yields first; `Some(id)` validates that a
154    /// matching package is contained.
155    fn resolve_main_id(&self, hint: Option<&str>) -> DamlResult<String> {
156        match hint {
157            Some(id) => {
158                if self.packages.iter().any(|p| p.hash() == id) {
159                    Ok(id.to_owned())
160                } else {
161                    Err(DamlError::Other(format!("main package {id} not present in package set")))
162                }
163            },
164            None => self
165                .packages
166                .first()
167                .map(|p| p.hash().to_owned())
168                .ok_or_else(|| DamlError::Other("expected at least one package".to_owned())),
169        }
170    }
171
172    fn payloads_by_id(&self) -> DamlResult<HashMap<String, DamlLfArchivePayload>> {
173        self.packages
174            .iter()
175            .map(|p| {
176                let hash = p.hash().to_owned();
177                let payload = DamlLfArchivePayload::from_bytes(p.payload().to_vec())
178                    .map_err(|e| DamlError::Other(e.to_string()))?;
179                Ok((hash, payload))
180            })
181            .collect()
182    }
183
184    /// BFS from `root` through the payload map, collecting the set of
185    /// package-ids that `root` transitively references. `root` itself
186    /// is included in the returned set. Package-ids referenced from
187    /// `root` that aren't in `payloads` are ignored — they may not be
188    /// available to this participant, or the payload map may already
189    /// be filtered.
190    fn reachable_from(root: &str, payloads: &HashMap<String, DamlLfArchivePayload>) -> DamlResult<HashSet<String>> {
191        let mut visited: HashSet<String> = HashSet::new();
192        let mut queue: VecDeque<String> = VecDeque::new();
193        queue.push_back(root.to_owned());
194        while let Some(id) = queue.pop_front() {
195            if !visited.insert(id.clone()) {
196                continue;
197            }
198            let Some(payload) = payloads.get(&id) else {
199                continue;
200            };
201            let refs = collect_referenced_package_ids(payload).map_err(|e| DamlError::Other(e.to_string()))?;
202            for r in refs {
203                if !visited.contains(&r) {
204                    queue.push_back(r);
205                }
206            }
207        }
208        Ok(visited)
209    }
210}
211
212/// Walk the decoded LF2 tree of `payload`, collecting every
213/// cross-package reference: type-constructor uses (`Absolute` and
214/// `NonLocal` variants) and value-name uses (`NonLocal` variant).
215/// The set is the payload's cross-package reference footprint.
216///
217/// `Local` variants (same-package) are ignored, and self-references
218/// via non-local names (source and target packages equal) are
219/// filtered out too.
220fn collect_referenced_package_ids(payload: &DamlLfArchivePayload) -> daml_lf::DamlLfResult<HashSet<String>> {
221    payload.clone().apply(|package: &DamlLfPackage<'_>| {
222        let mut visitor = ReferencedPackagesVisitor {
223            self_package_id: package.package_id().to_owned(),
224            referenced: HashSet::new(),
225        };
226        package.accept(&mut visitor);
227        visitor.referenced
228    })
229}
230
231struct ReferencedPackagesVisitor {
232    self_package_id: String,
233    referenced: HashSet<String>,
234}
235
236impl ReferencedPackagesVisitor {
237    fn record(&mut self, pkg: &str) {
238        if !pkg.is_empty() && pkg != self.self_package_id {
239            self.referenced.insert(pkg.to_owned());
240        }
241    }
242}
243
244impl DamlElementVisitor for ReferencedPackagesVisitor {
245    fn pre_visit_absolute_tycon<'a>(&mut self, abs: &'a DamlAbsoluteTyCon<'a>) {
246        self.record(abs.package_id());
247    }
248
249    fn pre_visit_non_local_tycon<'a>(&mut self, non_local: &'a DamlNonLocalTyCon<'a>) {
250        self.record(non_local.target_package_id());
251    }
252
253    fn pre_visit_non_local_value_name<'a>(&mut self, non_local: &'a DamlNonLocalValueName<'a>) {
254        self.record(non_local.target_package_id());
255    }
256}
257
258/// The automatic naming style to use when creating a `DamlLfArchive` from an unnamed `DamlPackage`.
259#[derive(Clone, Copy, Debug)]
260pub enum ArchiveAutoNamingStyle {
261    /// Name the `DamlLfArchive` with an empty String.
262    Empty,
263    /// Name the `DamlLfArchive` with the archive hash.
264    Hash,
265    /// Name the `DamlLfArchive` with a `uuid`.
266    Uuid,
267}
268
269/// Return the id of a package which contains a given module name or en error if no such package exists.
270///
271/// The supplied `module_name` name is assumed to be in `DottedName` format, i.e. `TopModule.SubModule.Module`.
272pub async fn find_module_package_id(ledger_client: &DamlGrpcClient, module_name: &str) -> DamlResult<String> {
273    DamlPackages::from_ledger(ledger_client).await?.find_module(module_name)
274}