1use alloc::{
2 boxed::Box,
3 string::{String, ToString},
4};
5#[cfg(feature = "std")]
6use std::path::Path;
7
8#[cfg(all(feature = "std", feature = "serde"))]
9use miden_assembly_syntax::debuginfo::Spanned;
10use miden_mast_package::PackageId;
11
12#[cfg(all(feature = "std", feature = "serde"))]
13use crate::ast::{ProjectFileError, WorkspaceFile};
14use crate::*;
15
16#[derive(Debug)]
18pub struct Package {
19 #[cfg(feature = "std")]
21 manifest_path: Option<Box<Path>>,
22 name: Span<PackageId>,
24 version: Span<SemVer>,
26 description: Option<Arc<str>>,
28 dependencies: Vec<Dependency>,
30 lints: MetadataSet,
34 metadata: MetadataSet,
38 lib: Option<Span<Target>>,
40 bins: Vec<Span<Target>>,
42 profiles: Vec<Profile>,
44}
45
46impl Package {
48 pub fn new(name: impl Into<PackageId>, default_target: Target) -> Box<Self> {
54 let name = name.into();
55 let (lib, bins) = if default_target.is_library() {
56 (Some(Span::unknown(default_target)), vec![])
57 } else {
58 (None, vec![Span::unknown(default_target)])
59 };
60 let profiles = vec![Profile::default(), Profile::release()];
61 Box::new(Self {
62 #[cfg(feature = "std")]
63 manifest_path: None,
64 name: Span::unknown(name),
65 version: Span::unknown(SemVer::new(0, 0, 0)),
66 description: None,
67 dependencies: Default::default(),
68 lints: Default::default(),
69 metadata: Default::default(),
70 lib,
71 bins,
72 profiles,
73 })
74 }
75
76 pub fn with_version(mut self: Box<Self>, version: SemVer) -> Box<Self> {
78 *self.version = version;
79 self
80 }
81
82 pub fn with_lints(mut self: Box<Self>, lints: MetadataSet) -> Box<Self> {
84 self.lints = lints;
85 self
86 }
87
88 pub fn with_metadata(mut self: Box<Self>, metadata: MetadataSet) -> Box<Self> {
90 self.metadata = metadata;
91 self
92 }
93
94 pub fn with_targets(
99 mut self: Box<Self>,
100 targets: impl IntoIterator<Item = Target>,
101 ) -> Box<Self> {
102 for target in targets {
103 if target.is_library() {
104 assert!(self.lib.is_none(), "a package cannot have duplicate library targets");
105 self.lib = Some(Span::unknown(target));
106 } else {
107 if self.bins.iter().any(|t| t.name == target.name) {
108 panic!("duplicate definitions of the same target '{}'", target.name);
109 }
110 self.bins.push(Span::unknown(target));
111 }
112 }
113 self
114 }
115
116 pub fn with_profile(mut self: Box<Self>, profile: Profile) -> Box<Self> {
120 for existing in self.profiles.iter_mut() {
121 if existing.name() == profile.name() {
122 existing.merge(&profile);
123 return self;
124 }
125 }
126
127 self.profiles.push(profile);
128 self
129 }
130
131 pub fn with_dependencies(
136 mut self: Box<Self>,
137 dependencies: impl IntoIterator<Item = Dependency>,
138 ) -> Box<Self> {
139 for dependency in dependencies {
140 if self.dependencies().iter().any(|dep| dep.name() == dependency.name()) {
141 panic!("duplicate definitions of dependency '{}'", dependency.name());
142 }
143 self.dependencies.push(dependency);
144 }
145
146 self
147 }
148}
149
150impl Package {
152 pub fn name(&self) -> Span<PackageId> {
154 self.name.clone()
155 }
156
157 pub fn version(&self) -> Span<&SemVer> {
159 self.version.as_ref()
160 }
161
162 pub fn description(&self) -> Option<Arc<str>> {
164 self.description.clone()
165 }
166
167 pub fn set_description(&mut self, description: impl Into<Arc<str>>) {
169 self.description = Some(description.into());
170 }
171
172 pub fn dependencies(&self) -> &[Dependency] {
174 &self.dependencies
175 }
176
177 pub fn num_dependencies(&self) -> usize {
179 self.dependencies.len()
180 }
181
182 pub fn lints(&self) -> &MetadataSet {
184 &self.lints
185 }
186
187 pub fn metadata(&self) -> &MetadataSet {
189 &self.metadata
190 }
191
192 pub fn profiles(&self) -> &[Profile] {
194 &self.profiles
195 }
196
197 pub fn get_profile(&self, name: &str) -> Option<&Profile> {
200 self.profiles().iter().find(|profile| profile.name().as_ref() == name)
201 }
202
203 pub fn resolve_profile(&self, name: &str) -> Result<&Profile, Report> {
206 self.get_profile(name).ok_or_else(|| {
207 Report::msg(format!(
208 "project '{}' does not define a '{}' build profile",
209 self.name().inner(),
210 name
211 ))
212 })
213 }
214
215 pub fn target_package_name(&self, target: &Target) -> PackageId {
217 if target.ty.is_executable() {
218 format!("{}:{}", self.name().inner(), target.name.inner()).into()
219 } else {
220 self.name().inner().clone()
221 }
222 }
223
224 pub fn library_target(&self) -> Option<&Span<Target>> {
226 self.lib.as_ref()
227 }
228
229 pub fn executable_targets(&self) -> &[Span<Target>] {
231 &self.bins
232 }
233
234 #[cfg(feature = "std")]
236 pub fn manifest_path(&self) -> Option<&Path> {
237 self.manifest_path.as_deref()
238 }
239
240 #[cfg(feature = "std")]
243 pub fn expect_manifest_path(&self) -> Result<&Path, Report> {
244 self.manifest_path().ok_or_else(|| {
245 Report::msg(format!("project '{}' is missing its manifest path", self.name().inner()))
246 })
247 }
248
249 pub fn build_provenance_projection(&self, target: &Target, profile: &Profile) -> String {
252 let Self {
253 #[cfg(feature = "std")]
254 manifest_path: _,
255 name,
256 version,
257 description: _,
258 dependencies: _,
259 lints: _,
260 metadata: _,
261 lib: _,
262 bins: _,
263 profiles: _,
264 } = self;
265
266 let mut projection = String::new();
267 projection.push_str("package:name:");
268 projection.push_str(name.inner().as_ref());
269 projection.push('\n');
270 projection.push_str("package:version:");
271 projection.push_str(version.inner().to_string().as_str());
272 projection.push('\n');
273 target.append_build_provenance_projection(&mut projection);
274 profile.append_build_provenance_projection(&mut projection);
275 projection
276 }
277}
278
279#[cfg(all(feature = "std", feature = "serde"))]
281impl Package {
282 pub fn load(source: Arc<SourceFile>) -> Result<Box<Self>, Report> {
285 Self::parse(source, None)
286 }
287
288 pub fn load_from_workspace(
291 source: Arc<SourceFile>,
292 workspace: &WorkspaceFile,
293 ) -> Result<Box<Self>, Report> {
294 Self::parse(source, Some(workspace))
295 }
296
297 fn parse(
298 source: Arc<SourceFile>,
299 workspace: Option<&WorkspaceFile>,
300 ) -> Result<Box<Self>, Report> {
301 let manifest_path = Path::new(source.uri().path());
302 let manifest_path = if manifest_path.try_exists().is_ok_and(|exists| exists) {
303 Some(manifest_path.to_path_buf().into_boxed_path())
304 } else {
305 None
306 };
307
308 let package_ast = ast::ProjectFile::parse(source.clone())?;
310
311 let version = package_ast.get_or_inherit_version(source.clone(), workspace)?;
313 let description = package_ast.get_or_inherit_description(source.clone(), workspace)?;
314
315 let mut profiles = Vec::default();
317 profiles.push(Profile::default());
318 profiles.push(Profile::release());
319 if let Some(workspace) = workspace {
320 for ast in workspace.profiles.iter() {
321 let profile = Profile::from_ast(ast, source.clone(), &profiles)?;
322 if let Some(prev) = profiles.iter_mut().find(|p| p.name() == ast.name.inner()) {
323 *prev = profile;
324 } else {
325 profiles.push(profile);
326 }
327 }
328 }
329
330 let package_profiles_start = profiles.len();
334 for ast in package_ast.profiles.iter() {
335 let profile = Profile::from_ast(ast, source.clone(), &profiles)?;
336
337 if let Some(prev_index) = profiles.iter().position(|p| p.name() == profile.name()) {
338 if prev_index < package_profiles_start {
339 profiles[prev_index].merge(&profile);
340 } else {
341 let prev = &profiles[prev_index];
342 return Err(ProjectFileError::DuplicateProfile {
343 name: prev.name().clone(),
344 source_file: source,
345 span: profile.span(),
346 prev: prev.span(),
347 }
348 .into());
349 }
350 } else {
351 profiles.push(profile);
352 }
353 }
354
355 let dependencies = package_ast.extract_dependencies(source.clone(), workspace)?;
358
359 let lib = package_ast.extract_library_target()?;
361 let bins = package_ast.extract_executable_targets();
362
363 let mut lints = workspace.map(|ws| ws.workspace.config.lints.clone()).unwrap_or_default();
364 lints.extend(package_ast.config.lints.clone());
365
366 let mut metadata =
367 workspace.map(|ws| ws.workspace.package.metadata.clone()).unwrap_or_default();
368 metadata.extend(package_ast.package.detail.metadata.clone());
369
370 Ok(Box::new(Self {
371 manifest_path,
372 name: package_ast.package.name.map(Into::into),
373 version,
374 description,
375 dependencies,
376 lints,
377 metadata,
378 profiles,
379 lib,
380 bins,
381 }))
382 }
383}
384
385#[cfg(feature = "serde")]
386impl Package {
387 pub fn to_toml(&self) -> Result<String, Report> {
393 let manifest_ast = ast::ProjectFile {
394 source_file: None,
395 package: ast::PackageTable {
396 name: self.name().map(PackageId::into_inner),
397 detail: ast::PackageDetail {
398 version: Some(
399 self.version().map(|v| ast::parsing::MaybeInherit::Value(v.clone())),
400 ),
401 description: self
402 .description()
403 .map(ast::parsing::MaybeInherit::Value)
404 .map(Span::unknown),
405 metadata: self.metadata.clone(),
406 },
407 },
408 config: ast::PackageConfig {
409 dependencies: self
410 .dependencies()
411 .iter()
412 .map(|dep| {
413 let name = Span::unknown(dep.name().clone());
414 let linkage = if matches!(dep.linkage(), Linkage::Dynamic) {
415 None
416 } else {
417 Some(Span::unknown(dep.linkage()))
418 };
419 let spec = match dep.scheme() {
420 DependencyVersionScheme::Workspace { .. } => ast::DependencySpec {
421 name: name.clone(),
422 version_or_digest: None,
423 workspace: true,
424 path: None,
425 git: None,
426 branch: None,
427 rev: None,
428 linkage,
429 },
430 DependencyVersionScheme::WorkspacePath { path, version } => {
431 ast::DependencySpec {
432 name: name.clone(),
433 version_or_digest: version.clone(),
434 workspace: false,
435 path: Some(path.clone()),
436 git: None,
437 branch: None,
438 rev: None,
439 linkage,
440 }
441 },
442 DependencyVersionScheme::Registry(req) => ast::DependencySpec {
443 name: name.clone(),
444 version_or_digest: Some(req.clone()),
445 workspace: false,
446 path: None,
447 git: None,
448 branch: None,
449 rev: None,
450 linkage,
451 },
452 DependencyVersionScheme::Path { path, version } => {
453 ast::DependencySpec {
454 name: name.clone(),
455 version_or_digest: version.clone(),
456 workspace: false,
457 path: Some(path.clone()),
458 git: None,
459 branch: None,
460 rev: None,
461 linkage,
462 }
463 },
464 DependencyVersionScheme::Git { repo, revision, version } => {
465 let (branch, rev) = match revision.inner() {
466 GitRevision::Branch(b) => {
467 (Some(Span::new(revision.span(), b.clone())), None)
468 },
469 GitRevision::Commit(c) => {
470 (None, Some(Span::new(revision.span(), c.clone())))
471 },
472 };
473 ast::DependencySpec {
474 name: name.clone(),
475 version_or_digest: version.as_ref().map(|spanned| {
476 VersionRequirement::from(spanned.inner().clone())
477 }),
478 workspace: false,
479 path: None,
480 git: Some(repo.clone()),
481 branch,
482 rev,
483 linkage,
484 }
485 },
486 };
487
488 (name, Span::unknown(spec))
489 })
490 .collect(),
491 lints: self.lints.clone(),
492 },
493 lib: self.lib.as_ref().map(|lib| {
494 Span::unknown(ast::LibTarget {
495 kind: if matches!(lib.ty, TargetType::Library) {
496 None
497 } else {
498 Some(Span::unknown(lib.ty))
499 },
500 namespace: Some(lib.namespace.as_ref().map(|path| path.as_str().into())),
501 path: lib.path.clone(),
502 })
503 }),
504 bins: self
505 .bins
506 .iter()
507 .map(|bin| {
508 Span::unknown(ast::BinTarget {
509 name: Some(bin.name.clone()),
510 path: bin.path.clone(),
511 })
512 })
513 .collect(),
514 profiles: self
515 .profiles()
516 .iter()
517 .map(|profile| ast::Profile {
518 inherits: None,
519 name: Span::unknown(profile.name().clone()),
520 debug: Some(profile.should_emit_debug_info()),
521 trim_paths: Some(profile.should_trim_paths()),
522 metadata: profile.metadata().clone(),
523 })
524 .collect(),
525 };
526
527 toml::to_string_pretty(&manifest_ast)
528 .map_err(|err| Report::msg(format!("failed to pretty print project manifest: {err}")))
529 }
530}