Skip to main content

openusd_build/
lib.rs

1//! Generates Rust schema views from OpenUSD `schema.usda` files, with the
2//! metadata fields and kinds a `plugInfo.json` declares beside them.
3//!
4//! This is the Rust answer to `usdGenSchema`, shaped as a build dependency: a
5//! consumer describes its schema libraries in `build.rs`, and the generated
6//! views join the crate through
7//! [`openusd::include_schema!`](openusd::include_schema).
8//!
9//! ```no_run
10//! // build.rs
11//! openusd_build::configure()
12//!     .search_path("schemas")
13//!     .schema("schemas/usdGeom/schema.usda")
14//!     .generate()?;
15//! # Ok::<(), openusd_build::Error>(())
16//! ```
17//!
18//! ```ignore
19//! // src/lib.rs
20//! pub mod geom {
21//!     openusd::include_schema!("usdGeom");
22//! }
23//! ```
24//!
25//! A library's name comes from the `libraryName` its `schema.usda` declares,
26//! not from the path, so the two spellings above name one library.
27//!
28//! [`Builder::plug_info`] adds what a library's plugin declares, and
29//! [`Builder::declaration_family`] generates a plugin that has declarations
30//! and no `schema.usda` at all.
31//!
32//! [`Builder::generate`] reads and checks the schemas it is given and writes
33//! one file per library, carrying both the views and the schema data behind
34//! them. The data is written as declarations a registry takes directly, so
35//! nothing is serialized on the way out and nothing is parsed on the way back
36//! in: what a consumer registers is a `const` its compiler has already checked.
37//!
38//! A registry is handed the whole of it through the `SCHEMAS` each generated
39//! library exposes:
40//!
41//! ```ignore
42//! use openusd::usd::SchemaRegistry;
43//!
44//! let registry = SchemaRegistry::builder().register(geom::SCHEMAS).build()?;
45//! ```
46//!
47//! Fenced `ignore`, as the `include_schema!` example above is: this crate has
48//! no build script, so there is no `OUT_DIR` here for the module to come from.
49
50mod error;
51
52mod decl;
53mod emit;
54mod load;
55mod plug_info;
56mod resolve;
57mod shader_defs;
58mod tokens;
59mod validate;
60
61mod doc;
62mod model;
63mod names;
64mod types;
65
66use std::collections::{BTreeMap, BTreeSet};
67use std::env;
68use std::fs;
69use std::path::{Path, PathBuf};
70use std::process;
71
72use openusd::{tf, usd};
73
74use crate::model::Library;
75
76/// Where a library's generated views live, for a base this run does not
77/// generate: the library name a schema declares, mapped to the Rust path its
78/// views are reachable at.
79pub(crate) type Externs = BTreeMap<String, String>;
80
81pub use error::{Error, TokenEnumError};
82pub use validate::Violation;
83
84/// What generating one library produced: a schema library, or the family of a
85/// plugin that declares no schema.
86///
87/// The declarations are the contract: a registry built from them answers what
88/// a registry built from upstream's own schema data answers. They reach a
89/// consumer as the `SCHEMAS` table in [`rust`](Self::rust), and this crate's
90/// own callers through [`with_family`](Self::with_family).
91#[derive(Debug)]
92pub struct Output {
93    /// The generated file: this library's tokens and schema data, and its views
94    /// unless they were left out.
95    pub rust: String,
96    /// Whether [`rust`](Self::rust) carries the views: [`Views::Skip`] was
97    /// asked for, or the schema declared `skipCodeGeneration`.
98    pub views: bool,
99    /// What a schema author should know, though the output is still correct.
100    pub warnings: Vec<String>,
101    /// The model everything above was generated from, and what
102    /// [`with_family`](Self::with_family) lends its declarations out of.
103    library: model::Library,
104    /// The file it was generated from: the schema as configured, or the
105    /// `plugInfo.json` of a plugin with no schema.
106    source: PathBuf,
107}
108
109impl Output {
110    /// The library's name: the `libraryName` its schema declared, or the name
111    /// of the plugin a declaration family was generated from.
112    pub fn library_name(&self) -> &str {
113        &self.library.name
114    }
115
116    /// Where every layer the schema resolved to was found, which is what a
117    /// build script watches. A layer reached through a
118    /// [`search_path`](Builder::search_path) is here by the location it was
119    /// found at, not the bare relative path the schema asked for.
120    pub fn layers(&self) -> &[PathBuf] {
121        &self.library.source_layers
122    }
123
124    /// Calls `f` with this library's schemas as a declared family — the schema
125    /// data itself, and the same declarations the generated table carries.
126    ///
127    /// A [`usd::SchemaFamily`] is a view over its parts, so it arrives through
128    /// a closure rather than as a value: what it borrows lives for the call.
129    /// [`usd::SchemaRegistryBuilder::register`] takes it, and
130    /// [`usd::SchemaFamily::to_data`] turns it into the class prims a
131    /// `generatedSchema.usda` would carry.
132    ///
133    /// ```no_run
134    /// # fn main() -> Result<(), openusd_build::Error> {
135    /// let output = openusd_build::configure()
136    ///     .build_library("schemas/usdGeom/schema.usda", openusd_build::Views::Generate)?;
137    /// let data = output.with_family(|family| family.to_data());
138    /// # let _ = data;
139    /// # Ok(())
140    /// # }
141    /// ```
142    pub fn with_family<R>(&self, f: impl FnOnce(&usd::SchemaFamily<'_>) -> R) -> R {
143        decl::with_family(&self.library, f)
144    }
145}
146
147/// What generating one shader-node library produced.
148#[derive(Debug)]
149pub struct NodeOutput {
150    /// The generated file: the node views and the tokens naming their ids,
151    /// inputs and outputs.
152    pub rust: String,
153    /// Where every layer the definitions composed from was found.
154    layers: Vec<PathBuf>,
155}
156
157impl NodeOutput {
158    /// Where every layer the definitions composed from was found, which is
159    /// what a build script watches.
160    pub fn layers(&self) -> &[PathBuf] {
161        &self.layers
162    }
163}
164
165/// Whether a library's views are generated beside its schema data.
166///
167/// The data is not optional either way: it is what a registry is built from, so
168/// a library declining an API still ships what registers it.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum Views {
171    /// Generate the trait and view per schema.
172    Generate,
173    /// Generate the tokens and the schema data alone, for a library whose API
174    /// is written by hand. A schema declaring `skipCodeGeneration` is treated
175    /// this way however it was asked for.
176    Skip,
177}
178
179/// A Rust enum over one token attribute's `allowedTokens`.
180///
181/// Generation is opt-in and explicit, because a token set is not an identity:
182/// two properties can admit the same tokens and mean different things, and two
183/// properties meaning the same thing can declare different fallbacks. So the
184/// enum is named here and sourced from the one property that defines it.
185/// Nothing else is associated with it — a property admitting the same tokens is
186/// free to use it or not, and the generated accessors are unchanged either
187/// way.
188///
189/// ```no_run
190/// # fn main() -> Result<(), openusd_build::Error> {
191/// use openusd_build::TokenEnum;
192///
193/// openusd_build::configure()
194///     .schema("schemas/usdGeom/schema.usda")
195///     .token_enum(TokenEnum::new("Purpose", "usdGeom", "Imageable.purpose").with_default())
196///     .generate()?;
197/// # Ok(())
198/// # }
199/// ```
200#[derive(Debug, Clone)]
201pub struct TokenEnum {
202    /// The Rust type to generate.
203    pub(crate) name: String,
204    /// The `libraryName` of the schema declaring the source property.
205    pub(crate) library: String,
206    /// `Class.property`, naming the declaration the variants come from.
207    source: String,
208    /// Whether to derive `Default` from the source property's fallback.
209    pub(crate) default: bool,
210    /// Variant names the caller spelled, by the token each stands for.
211    pub(crate) variants: BTreeMap<String, String>,
212}
213
214impl TokenEnum {
215    /// An enum called `name` over the `allowedTokens` of `source`, a property
216    /// of `library` named `Class.property`.
217    ///
218    /// `Class` is the schema's registered identifier, as the class prim spells
219    /// it, not the Rust name its `className` may ask for.
220    pub fn new(name: impl Into<String>, library: impl Into<String>, source: impl Into<String>) -> Self {
221        TokenEnum {
222            name: name.into(),
223            library: library.into(),
224            source: source.into(),
225            default: false,
226            variants: BTreeMap::new(),
227        }
228    }
229
230    /// Derives `Default` from the source property's fallback.
231    ///
232    /// Off unless asked for: a token set carries no default of its own, and the
233    /// corpus shows why — `guideVisibility` falls back to `invisible` where
234    /// `proxyVisibility` and `renderVisibility` fall back to `inherited`, all
235    /// three admitting the same tokens. So the default is the source
236    /// property's, said explicitly, and a source with no token fallback is an
237    /// error rather than a guess.
238    #[must_use]
239    pub fn with_default(mut self) -> Self {
240        self.default = true;
241        self
242    }
243
244    /// Spells the variant for `token` rather than casing it.
245    ///
246    /// For a token Rust has no name for — an empty spelling, one opening with a
247    /// digit — and for two tokens that would otherwise reach one name. Naming a
248    /// token the source property does not admit is an error, so an override
249    /// cannot quietly do nothing.
250    #[must_use]
251    pub fn variant(mut self, token: impl Into<String>, variant: impl Into<String>) -> Self {
252        self.variants.insert(token.into(), variant.into());
253        self
254    }
255
256    /// The class and property its variants come from, or the spelling that is
257    /// neither.
258    pub(crate) fn source(&self) -> Result<(&str, &str), TokenEnumError> {
259        self.source
260            .split_once('.')
261            .filter(|(class, property)| !class.is_empty() && !property.is_empty())
262            .ok_or_else(|| TokenEnumError::MalformedSource {
263                spelling: self.source.clone(),
264            })
265    }
266}
267
268/// Starts describing what to generate. See [`Builder`].
269pub fn configure() -> Builder {
270    Builder {
271        out_dir: None,
272        search_paths: Vec::new(),
273        extern_libraries: BTreeMap::new(),
274        schemas: Vec::new(),
275        token_enums: Vec::new(),
276        plug_infos: Vec::new(),
277        declaration_families: Vec::new(),
278        shader_defs: Vec::new(),
279    }
280}
281
282/// What to generate, and where.
283///
284/// Every method takes and returns the builder, so a `build.rs` reads as one
285/// expression.
286#[derive(Debug)]
287pub struct Builder {
288    out_dir: Option<PathBuf>,
289    search_paths: Vec<PathBuf>,
290    extern_libraries: Externs,
291    /// Each schema to generate, and whether its views are wanted with it.
292    schemas: Vec<(PathBuf, Views)>,
293    /// The enums to generate over token properties, in the order declared.
294    token_enums: Vec<TokenEnum>,
295    /// The `plugInfo.json` files declaring libraries' metadata fields and
296    /// kinds.
297    plug_infos: Vec<PathBuf>,
298    /// The plugins to generate as families of their own, with no schema
299    /// library behind them.
300    declaration_families: Vec<String>,
301    /// The shader-definition layers to generate node views of, each with the
302    /// library name its file is written under.
303    shader_defs: Vec<(String, PathBuf)>,
304}
305
306impl Builder {
307    /// Writes generated files to `dir` instead of `OUT_DIR`.
308    ///
309    /// A build script needs this only when it writes somewhere it also reads
310    /// from, such as a checked-in copy; `OUT_DIR` is the ordinary destination,
311    /// and where [`openusd::include_schema!`](openusd::include_schema) looks.
312    #[must_use]
313    pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
314        self.out_dir = Some(dir.into());
315        self
316    }
317
318    /// Reads what a `plugInfo.json` declares beside its schemas, the way C++
319    /// registers it: the metadata fields of each plugin's `SdfMetadata` block
320    /// and the kinds of its `Kinds` block.
321    ///
322    /// Each plugin's declarations belong to the library whose `libraryName`
323    /// is the plugin's `Name`. They are registered with that library's
324    /// schemas; a field is also named in its `tokens`, and read and written
325    /// through extension traits on the handles it applies to
326    /// (`AttributeMetadata` and its siblings). A plugin with no schema library
327    /// is named with [`declaration_family`](Self::declaration_family), and one
328    /// file may hold plugins of both sorts. A plugin that is neither is an
329    /// error when [`generate`](Self::generate) runs.
330    #[must_use]
331    pub fn plug_info(mut self, path: impl Into<PathBuf>) -> Self {
332        self.plug_infos.push(path.into());
333        self
334    }
335
336    /// Generates the plugin called `name` as a family of its own, for a
337    /// plugin that declares kinds or metadata fields and has no schema
338    /// library. Repeatable.
339    ///
340    /// The plugin is read from a file [`plug_info`](Self::plug_info) names.
341    /// Its file is written as `<name>.rs` and included with
342    /// [`openusd::include_schema!`](openusd::include_schema) like a schema
343    /// library's, and its `SCHEMAS` registers the declarations and no schema.
344    /// A name no configured `plugInfo.json` declares is an error when
345    /// [`generate`](Self::generate) runs.
346    #[must_use]
347    pub fn declaration_family(mut self, name: impl Into<String>) -> Self {
348        self.declaration_families.push(name.into());
349        self
350    }
351
352    /// Generates a view of each shader node the layer at `path` defines, as
353    /// `usdShaders`'s `shaderDefs.usda` defines `UsdPreviewSurface` and its
354    /// companions.
355    ///
356    /// Each `def Shader` prim authoring an `info:id` is a node, and its view
357    /// wraps the `usdShade` library's `Shader` view, so that library must be
358    /// placed with [`extern_library`](Self::extern_library). The file is
359    /// written as `<library>.rs` and included with
360    /// [`openusd::include_schema!`](openusd::include_schema) like a schema
361    /// library's.
362    #[must_use]
363    pub fn shader_defs(mut self, library: impl Into<String>, path: impl Into<PathBuf>) -> Self {
364        self.shader_defs.push((library.into(), path.into()));
365        self
366    }
367
368    /// Adds a directory that a `schema.usda`'s sublayers resolve through, the
369    /// way [`ar::DefaultResolver::with_search_paths`](openusd::ar::DefaultResolver::with_search_paths)
370    /// takes them.
371    ///
372    /// Upstream schemas sublayer each other by bare path
373    /// (`subLayers = [@usd/schema.usda@]`), which is what a search path
374    /// resolves. Repeatable, and searched in the order given.
375    #[must_use]
376    pub fn search_path(mut self, dir: impl Into<PathBuf>) -> Self {
377        self.search_paths.push(dir.into());
378        self
379    }
380
381    /// Declares where a library this run does not generate already lives, so
382    /// the generated code can name types from it.
383    ///
384    /// `library` is the `libraryName` that library's `schema.usda` declares and
385    /// `rust_path` the module path its views are reachable at, as in
386    /// `("usdGeom", "openusd_schemas::geom")`. A class inheriting from, or
387    /// reflecting, a schema of a library this run does not generate needs it
388    /// declared here; a class of a declared library that a schema's prose names
389    /// is linked to where it lives, and one of an undeclared library is left as
390    /// prose. The path is to the library's views, so a library generated for
391    /// its schema data alone has nothing to declare. Keyed by library name, so
392    /// declaring one twice keeps the last.
393    #[must_use]
394    pub fn extern_library(mut self, library: impl Into<String>, rust_path: impl Into<String>) -> Self {
395        self.extern_libraries.insert(library.into(), rust_path.into());
396        self
397    }
398
399    /// Adds a `schema.usda` to generate a library from. Repeatable.
400    #[must_use]
401    pub fn schema(mut self, path: impl Into<PathBuf>) -> Self {
402        self.schemas.push((path.into(), Views::Generate));
403        self
404    }
405
406    /// Adds a `schema.usda` to generate schema data from, without its views.
407    ///
408    /// The generated file still carries the tokens and the declaration table a
409    /// registry is built from; what it leaves out is the trait and view per
410    /// schema. That is what a crate whose API for these schemas is written by
411    /// hand wants: the declarations a registry needs, without a generated set
412    /// of views beside its own to collide with.
413    ///
414    /// A schema that declares `skipCodeGeneration` is treated this way whether
415    /// or not it is named here.
416    #[must_use]
417    pub fn schema_data(mut self, path: impl Into<PathBuf>) -> Self {
418        self.schemas.push((path.into(), Views::Skip));
419        self
420    }
421
422    /// Adds a Rust enum over a token property's `allowedTokens`. Repeatable.
423    ///
424    /// See [`TokenEnum`] for what it takes and why it is named rather than
425    /// derived. The enum joins the generated file of the library its source
426    /// property belongs to, beside that library's token constants; a library no
427    /// configured enum names generates exactly what it did before.
428    ///
429    /// An enum whose library this run does not build generates nothing, so one
430    /// list of enums describes families that are each behind their own
431    /// feature. Naming a library this build neither generates nor declares
432    /// with [`extern_library`](Self::extern_library) is an error.
433    #[must_use]
434    pub fn token_enum(mut self, declared: TokenEnum) -> Self {
435        self.token_enums.push(declared);
436        self
437    }
438
439    /// Generates every configured library.
440    ///
441    /// Configuring no schemas, shader definitions or declaration families is
442    /// not an error and writes nothing: a consumer whose families are
443    /// feature-gated generates none of them when its features are off, and its
444    /// `build.rs` should not have to know that.
445    ///
446    /// Otherwise the output directory must be known, from
447    /// [`out_dir`](Self::out_dir) or from the `OUT_DIR` a build script runs
448    /// with, and is created if it does not exist.
449    ///
450    /// Each library writes one file, `<library>.rs`, named after the
451    /// `libraryName` its `/GLOBAL` declares and included by
452    /// [`openusd::include_schema!`](openusd::include_schema). It carries both
453    /// the views and the schema data, the latter as the declaration table
454    /// [`usd::SchemaRegistryBuilder::register`] takes. A library that asked for
455    /// `skipCodeGeneration` writes that file too, carrying its tokens and its
456    /// schema data without the views — the data is what a registry is built
457    /// from, so a library declining an API still has to ship it.
458    ///
459    /// Two schemas declaring the same `libraryName` would write one file, and
460    /// are refused before anything is written. A file already holding what
461    /// this run generates is left untouched, its modification time with it.
462    pub fn generate(self) -> Result<(), Error> {
463        if self.schemas.is_empty() && self.shader_defs.is_empty() && self.declaration_families.is_empty() {
464            return Ok(());
465        }
466
467        let out_dir = match &self.out_dir {
468            Some(dir) => dir.clone(),
469            None => env::var_os("OUT_DIR").map(PathBuf::from).ok_or(Error::NoOutDir)?,
470        };
471        fs::create_dir_all(&out_dir).map_err(|source| Error::Io {
472            path: out_dir.clone(),
473            source,
474        })?;
475
476        // Every library is generated before any of them is written, so a
477        // schema this run refuses leaves the last run's output alone rather
478        // than half of it replaced. Each file is then replaced whole (`write`),
479        // so a run failing part-way leaves every library as one run or the
480        // other wrote it.
481        //
482        // TODO(rayon): a library is an independent stage open, composition and
483        // emission over `&self`, so the families of `openusd-schemas` could
484        // build at once. `Output` is what stands in the way: its model holds
485        // `sdf::Value`s read from a stage that is neither `Send` nor `Sync`, so
486        // a parallel version has to finish with each library inside the worker
487        // and return the generated text alone.
488        let plugins = self.plugins()?;
489        let mut outputs = self
490            .schemas
491            .iter()
492            .map(|(schema, views)| self.build(schema, *views, &plugins))
493            .collect::<Result<Vec<_>, Error>>()?;
494        for family in &self.declaration_families {
495            outputs.push(self.declare(family, &plugins)?);
496        }
497
498        // A library this run builds, or one declared so a view can name it: an
499        // enum naming anything else matches nothing, ever. A family behind
500        // a feature that is off is still declared, so this does not catch
501        // one that is simply switched off.
502        let known: BTreeSet<&str> = outputs
503            .iter()
504            .map(Output::library_name)
505            .chain(self.extern_libraries.keys().map(String::as_str))
506            .collect();
507        if let Some(declared) = self
508            .token_enums
509            .iter()
510            .find(|declared| !known.contains(declared.library.as_str()))
511        {
512            return Err(Error::TokenEnum {
513                name: declared.name.clone(),
514                cause: TokenEnumError::UnknownLibrary {
515                    library: declared.library.clone(),
516                },
517            });
518        }
519
520        // Every plugin a `plugInfo.json` declares is one of the libraries or
521        // declaration families built here.
522        for plugin in &plugins {
523            if !outputs.iter().any(|output| output.library_name() == plugin.name) {
524                return Err(Error::UnknownPlugin {
525                    plugin: plugin.name.clone(),
526                    path: plugin.path.clone(),
527                });
528            }
529        }
530
531        let nodes = self
532            .shader_defs
533            .iter()
534            .map(|(library, path)| Ok((library.as_str(), path.as_path(), self.build_shader_defs(library, path)?)))
535            .collect::<Result<Vec<_>, Error>>()?;
536
537        // One file per library name, so two libraries of one name would write
538        // the same file.
539        let mut destinations: BTreeMap<&str, &Path> = BTreeMap::new();
540        let named = outputs
541            .iter()
542            .map(|output| (output.library_name(), output.source.as_path()))
543            .chain(nodes.iter().map(|(library, path, _)| (*library, *path)));
544        for (library, source) in named {
545            if let Some(first) = destinations.insert(library, source) {
546                return Err(Error::DuplicateLibrary {
547                    library: library.to_owned(),
548                    first: first.to_path_buf(),
549                    second: source.to_path_buf(),
550                });
551            }
552        }
553
554        // A field or a kind is declared once. Two plugins of one name are one
555        // library, so a repeat between them names that library twice.
556        let fields = declared_twice(&outputs, |library| {
557            library.metadata.iter().map(|field| field.name.as_str()).collect()
558        });
559        if let Some((field, first, second)) = fields {
560            return Err(Error::DuplicateMetadataField { field, first, second });
561        }
562        let kinds = declared_twice(&outputs, |library| {
563            library.kinds.iter().map(|kind| kind.name.as_str()).collect()
564        });
565        if let Some((kind, first, second)) = kinds {
566            return Err(Error::DuplicateKind { kind, first, second });
567        }
568        for (library, _, output) in &nodes {
569            write(&out_dir.join(format!("{library}.rs")), &output.rust)?;
570            for layer in output.layers() {
571                println!("cargo:rerun-if-changed={}", layer.display());
572            }
573        }
574
575        for output in &outputs {
576            for warning in &output.warnings {
577                println!("cargo:warning={warning}");
578            }
579
580            write(&out_dir.join(format!("{}.rs", output.library_name())), &output.rust)?;
581
582            // Every layer the schema composed from, so an edit to a sublayer
583            // regenerates as surely as an edit to the file named here. Naming
584            // any of them switches off cargo's own "rerun if the package
585            // changed" default, which is why the whole stack is named and not
586            // the configured paths alone.
587            for layer in output.layers() {
588                println!("cargo:rerun-if-changed={}", layer.display());
589            }
590        }
591        Ok(())
592    }
593
594    /// Generates one schema library, writing nothing.
595    ///
596    /// The whole pipeline, for a caller that wants the result rather than the
597    /// files: [`generate`](Self::generate) runs this over every schema
598    /// [`schema`](Self::schema) named and writes what comes back. The schema is
599    /// named here instead, so that list has no bearing on this call.
600    /// A schema asking for `skipCodeGeneration` gets no views whatever `views`
601    /// says.
602    pub fn build_library(&self, schema: impl AsRef<Path>, views: Views) -> Result<Output, Error> {
603        self.build(schema.as_ref(), views, &self.plugins()?)
604    }
605
606    /// Generates the declaration-only family of the plugin called `family`,
607    /// writing nothing: the file [`generate`](Self::generate) writes for a
608    /// name [`declaration_family`](Self::declaration_family) gave.
609    pub fn build_declarations(&self, family: &str) -> Result<Output, Error> {
610        self.declare(family, &self.plugins()?)
611    }
612
613    /// Generates the family of what the plugins called `family` declare, with
614    /// no schema library behind it.
615    fn declare(&self, family: &str, plugins: &[plug_info::Plugin]) -> Result<Output, Error> {
616        // The name is the generated file's, and what `include_schema!` is
617        // given, so it has to be one a path and a Rust string both take whole.
618        if !tf::is_valid_identifier(family) {
619            return Err(Error::InvalidDeclarationFamily {
620                family: family.to_owned(),
621            });
622        }
623        let Some(plugin) = plugins.iter().find(|plugin| plugin.name == family) else {
624            return Err(Error::UnknownDeclarationFamily {
625                family: family.to_owned(),
626            });
627        };
628        let mut library = Library::declarations(family);
629        library.take_declarations(plugins);
630        let warnings = validate::check(&library)?;
631        // The traits a metadata field is read through are emitted with the
632        // views, so the views are asked for.
633        self.emit(library, warnings, &plugin.path, Views::Generate)
634    }
635
636    /// Generates the file of a library read from `source`.
637    fn emit(&self, library: Library, warnings: Vec<String>, source: &Path, views: Views) -> Result<Output, Error> {
638        // Named by file rather than by path: the header reaches whatever the
639        // consumer generates into, and an absolute path would differ on every
640        // machine that built it.
641        let named = source.file_name().unwrap_or(source.as_os_str());
642        let rust = emit::emit(
643            &library,
644            &self.extern_libraries,
645            &named.to_string_lossy(),
646            views,
647            &self.token_enums,
648        )?;
649        Ok(Output {
650            rust,
651            views: views == Views::Generate,
652            warnings,
653            library,
654            source: source.to_path_buf(),
655        })
656    }
657
658    /// Every plugin the configured `plugInfo.json` files declare, each file
659    /// read once.
660    fn plugins(&self) -> Result<Vec<plug_info::Plugin>, Error> {
661        let mut plugins = Vec::new();
662        for path in &self.plug_infos {
663            plugins.extend(plug_info::read(path)?);
664        }
665        Ok(plugins)
666    }
667
668    /// Generates the views of the shader nodes the layer at `path` defines,
669    /// writing nothing: the file [`generate`](Self::generate) writes for a
670    /// library [`shader_defs`](Self::shader_defs) named.
671    pub fn build_shader_defs(&self, library: &str, path: impl AsRef<Path>) -> Result<NodeOutput, Error> {
672        let path = path.as_ref();
673        let shade = self.extern_libraries.get("usdShade").ok_or_else(|| Error::ShaderDefs {
674            path: path.to_path_buf(),
675            cause: "a node's view wraps `usdShade`'s `Shader`, and no extern_library places usdShade".to_owned(),
676        })?;
677        let shade: syn::Path = syn::parse_str(shade).map_err(|_| Error::ShaderDefs {
678            path: path.to_path_buf(),
679            cause: format!("usdShade is placed at `{shade}`, which is not a Rust path"),
680        })?;
681        let library = shader_defs::read(self, library, path)?;
682        let rust = emit::nodes(&library, &shade)?;
683        Ok(NodeOutput {
684            rust,
685            layers: library.layers,
686        })
687    }
688
689    /// Reads, checks and generates one library.
690    fn build(&self, schema: &Path, views: Views, plugins: &[plug_info::Plugin]) -> Result<Output, Error> {
691        let (library, warnings) = self.read(schema, plugins)?;
692        let views = match library.skip_code_generation {
693            true => Views::Skip,
694            false => views,
695        };
696        self.emit(library, warnings, schema, views)
697    }
698
699    /// Reads one schema into the model every output is generated from, with
700    /// whatever a schema author should know about it.
701    ///
702    /// The three stages behind it stay separate: [`load`] extracts what the
703    /// layers declare, [`resolve`] consults composition once and keeps the
704    /// answers, and [`validate`] checks the rules. What validation finds that
705    /// does not make the output wrong travels back beside the model, so a
706    /// caller reading the result in memory sees it as well as a build log does.
707    fn read(&self, schema: &Path, plugins: &[plug_info::Plugin]) -> Result<(Library, Vec<String>), Error> {
708        let source = load::open(self, schema)?;
709        let mut library = resolve::library(&source)?;
710
711        library.take_declarations(plugins);
712
713        // Prim indices are built on demand, so a diagnostic a class's own
714        // composition raises exists only once resolve has read that class.
715        // Asking again here is what catches those.
716        Error::composition(schema, &source.stage).map_or(Ok(()), Err)?;
717
718        let warnings = validate::check(&library)?;
719        Ok((library, warnings))
720    }
721}
722
723/// The first name two of `outputs` both declare, with the library declaring
724/// it first and the one declaring it again. `names` lists what one library
725/// declares.
726fn declared_twice<'a>(
727    outputs: &'a [Output],
728    names: impl Fn(&'a Library) -> Vec<&'a str>,
729) -> Option<(String, String, String)> {
730    let mut declared: BTreeMap<&str, &str> = BTreeMap::new();
731    for output in outputs {
732        for name in names(&output.library) {
733            if let Some(first) = declared.insert(name, output.library_name()) {
734                return Some((name.to_owned(), first.to_owned(), output.library_name().to_owned()));
735            }
736        }
737    }
738    None
739}
740
741/// Writes one generated file, naming the path in anything that goes wrong.
742///
743/// A file already holding `contents` is left untouched, so whatever watches
744/// its modification time — rustc's incremental cache, an editor — sees
745/// nothing change when the same text is regenerated. Anything else is written
746/// beside the destination and renamed onto it, which replaces the file whole:
747/// an interrupted run leaves the previous file or the new one, never a
748/// truncated one.
749fn write(path: &Path, contents: &str) -> Result<(), Error> {
750    if fs::read(path).is_ok_and(|current| current == contents.as_bytes()) {
751        return Ok(());
752    }
753
754    let mut staged = path.as_os_str().to_owned();
755    staged.push(format!(".tmp-{}", process::id()));
756    let staged = PathBuf::from(staged);
757    let written = fs::write(&staged, contents).map_err(|source| Error::Io {
758        path: staged.clone(),
759        source,
760    });
761    let replaced = written.and_then(|()| {
762        fs::rename(&staged, path).map_err(|source| Error::Io {
763            path: path.to_path_buf(),
764            source,
765        })
766    });
767    if replaced.is_err() {
768        // Nothing to report beyond the failure itself, which is the one a
769        // contributor needs; a stale staging file only costs disk space.
770        let _ = fs::remove_file(&staged);
771    }
772    replaced
773}
774
775#[cfg(test)]
776mod tests {
777
778    use super::*;
779
780    use std::time::{Duration, SystemTime};
781
782    use openusd::sdf;
783
784    /// Resolves one file of the vendored upstream corpus, which every module's
785    /// tests read.
786    pub(crate) fn read_fixture(name: &str) -> Result<Library, Error> {
787        let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/testUsdGenSchema");
788        configure()
789            .search_path(&dir)
790            .read(&dir.join(name), &[])
791            .map(|(library, _)| library)
792    }
793
794    /// Resolves a schema a test writes for itself, for a case the vendored
795    /// corpus does not reach.
796    pub(crate) fn read_source(dir: &Path, source: &str) -> Result<Library, Error> {
797        fs::write(dir.join("schema.usda"), source).expect("writes the schema");
798        configure()
799            .read(&dir.join("schema.usda"), &[])
800            .map(|(library, _)| library)
801    }
802
803    /// A schema library called `library`: the roots, and whatever `classes`
804    /// declare.
805    pub(crate) fn schema(library: &str, classes: &str) -> String {
806        format!(
807            r#"#usda 1.0
808
809def "GLOBAL" (
810    customData = {{
811        string libraryName = "{library}"
812    }}
813)
814{{
815}}
816
817class "Typed" {{}}
818
819class "APISchemaBase" {{}}
820
821{classes}
822"#
823        )
824    }
825
826    /// A single-apply API schema for a class to reflect, declaring `dup` among
827    /// its properties for a second reflected schema to collide with.
828    pub(crate) const TAG_API: &str = r#"class "TagAPI" (
829    inherits = </APISchemaBase>
830    customData = { token apiSchemaType = "singleApply" }
831) {
832    string tag = ""
833    int dup = 0
834}"#;
835
836    /// An API schema whose name breaks the suffix convention, which is a
837    /// warning rather than a rule: the registry never reads the spelling.
838    const MISSING_SUFFIX: &str = r#"#usda 1.0
839
840def "GLOBAL" (
841    customData = {
842        string libraryName = "testWarn"
843    }
844)
845{
846}
847
848class "APISchemaBase"
849{
850}
851
852class "MissingSuffix" (
853    inherits = </APISchemaBase>
854    customData = {
855        token apiSchemaType = "singleApply"
856    }
857)
858{
859}
860"#;
861
862    /// A convention warning reaches the caller that asked for the library, not
863    /// only the build log a `generate` run prints to.
864    #[test]
865    fn build_library_reports_warnings() {
866        let dir = tempfile::tempdir().expect("tempdir");
867        let path = dir.path().join("schema.usda");
868        fs::write(&path, MISSING_SUFFIX).expect("writes the schema");
869
870        let output = configure().build_library(&path, Views::Generate).expect("builds");
871        let warnings = &output.warnings;
872        assert_eq!(warnings.len(), 1, "{warnings:?}");
873        assert!(warnings[0].contains("conventionally ends in `API`"), "{warnings:?}");
874    }
875
876    /// A run with nothing configured writes nothing and needs no destination,
877    /// which is the state a consumer's build script is in when every schema
878    /// family it can generate is behind a feature that is off.
879    #[test]
880    fn no_schemas_writes_nothing() {
881        let dir = tempfile::tempdir().expect("tempdir");
882        let out = dir.path().join("generated");
883
884        configure().out_dir(&out).generate().expect("nothing to do");
885
886        assert!(!out.exists(), "a run with no schemas creates no output directory");
887    }
888
889    /// The destination is created on demand, since `OUT_DIR` exists but a
890    /// caller's own path need not.
891    #[test]
892    fn out_dir_created() {
893        let dir = tempfile::tempdir().expect("tempdir");
894        let out = dir.path().join("nested/generated");
895
896        // The corpus reflects a schema of its `usd` sublayer library, whose
897        // views the generated code has to be able to name.
898        let schema = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/testUsdGenSchema/schema.usda");
899        configure()
900            .out_dir(&out)
901            .search_path(schema.parent().expect("a parent"))
902            .extern_library("usd", "::openusd::usd")
903            .schema(&schema)
904            .generate()
905            .expect("generates");
906
907        assert!(out.is_dir(), "the output directory is created if it is missing");
908    }
909
910    /// `MISSING_SUFFIX` written to `dir`, and the library it declares.
911    fn missing_suffix(dir: &Path) -> PathBuf {
912        let path = dir.join("schema.usda");
913        fs::write(&path, MISSING_SUFFIX).expect("writes the schema");
914        path
915    }
916
917    /// What `generate` writes is what `build_library` hands back, with no
918    /// staging file left beside it.
919    #[test]
920    fn generated_file_written() {
921        let dir = tempfile::tempdir().expect("tempdir");
922        let schema = missing_suffix(dir.path());
923        let out = dir.path().join("generated");
924
925        configure().out_dir(&out).schema(&schema).generate().expect("generates");
926
927        let expected = configure()
928            .build_library(&schema, Views::Generate)
929            .expect("builds")
930            .rust;
931        let written = fs::read_to_string(out.join("testWarn.rs")).expect("reads the output");
932        assert_eq!(written, expected);
933        let names: Vec<_> = fs::read_dir(&out)
934            .expect("lists the output")
935            .map(|entry| entry.expect("an entry").file_name())
936            .collect();
937        assert_eq!(names, vec!["testWarn.rs"], "nothing else is left behind");
938    }
939
940    /// Regenerating the same text leaves the file as it was, modification
941    /// time included; different text replaces it.
942    #[test]
943    fn unchanged_file_untouched() {
944        let dir = tempfile::tempdir().expect("tempdir");
945        let schema = missing_suffix(dir.path());
946        let out = dir.path().join("generated");
947        let file = out.join("testWarn.rs");
948        let generate = || configure().out_dir(&out).schema(&schema).generate();
949
950        generate().expect("generates");
951        let long_ago = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
952        fs::File::options()
953            .write(true)
954            .open(&file)
955            .and_then(|written| written.set_modified(long_ago))
956            .expect("backdates the output");
957
958        generate().expect("regenerates");
959        let modified = || {
960            fs::metadata(&file)
961                .and_then(|meta| meta.modified())
962                .expect("reads mtime")
963        };
964        assert_eq!(modified(), long_ago, "the same text is not written again");
965
966        let changed = MISSING_SUFFIX.replace(
967            "class \"MissingSuffix\" (",
968            "class \"MissingSuffix\" (\n    doc = \"Changed.\"",
969        );
970        fs::write(&schema, changed).expect("edits the schema");
971        generate().expect("regenerates");
972        assert_ne!(modified(), long_ago, "different text replaces the file");
973        assert!(fs::read_to_string(&file).expect("reads").contains("Changed."));
974    }
975
976    /// Two schemas declaring one `libraryName` would write one file, so the
977    /// run refuses before writing either.
978    #[test]
979    fn duplicate_library_refused() {
980        let dir = tempfile::tempdir().expect("tempdir");
981        let [first, second] = ["a", "b"].map(|name| {
982            let own = dir.path().join(name);
983            fs::create_dir(&own).expect("creates a schema directory");
984            missing_suffix(&own)
985        });
986        let out = dir.path().join("generated");
987
988        let error = configure()
989            .out_dir(&out)
990            .schema(&first)
991            .schema(&second)
992            .generate()
993            .expect_err("both declare testWarn");
994
995        match error {
996            Error::DuplicateLibrary {
997                library,
998                first: named_first,
999                second: named_second,
1000            } => {
1001                assert_eq!(library, "testWarn");
1002                assert_eq!((named_first, named_second), (first, second));
1003            }
1004            other => panic!("{other}"),
1005        }
1006        assert!(!out.join("testWarn.rs").exists(), "nothing is written");
1007    }
1008
1009    /// A `plugInfo.json` declaring `field` for the plugin `plugin`, written
1010    /// to `dir`.
1011    fn plug_info(dir: &Path, plugin: &str, field: &str) -> PathBuf {
1012        let path = dir.join(format!("{plugin}-{field}.json"));
1013        let text = format!(
1014            r#"{{"Plugins": [{{"Name": "{plugin}", "Info": {{"SdfMetadata": {{"{field}": {{"type": "token"}}}}}}}}]}}"#
1015        );
1016        fs::write(&path, text).expect("writes the plugInfo");
1017        path
1018    }
1019
1020    /// A `plugInfo.json` holding the schema library `testWarn`'s plugin and
1021    /// the plugin `siteKinds`, which declares kinds and has no schema library.
1022    fn mixed_plug_info(dir: &Path) -> PathBuf {
1023        let path = dir.join("mixed.json");
1024        fs::write(
1025            &path,
1026            r#"{"Plugins": [
1027                {"Name": "testWarn", "Info": {
1028                    "SdfMetadata": {"role": {"type": "token"}},
1029                    "Kinds": {"prop": {"baseKind": "component"}}
1030                }},
1031                {"Name": "siteKinds", "Info": {"Kinds": {
1032                    "chargroup": {"baseKind": "assembly"},
1033                    "site_root": {}
1034                }}}
1035            ]}"#,
1036        )
1037        .expect("writes the plugInfo");
1038        path
1039    }
1040
1041    /// The kinds `output`'s family declares, each with its base.
1042    fn declared_kinds(output: &Output) -> Vec<(String, Option<String>)> {
1043        output.with_family(|family| {
1044            family
1045                .declared_kinds()
1046                .iter()
1047                .map(|kind| (kind.name().to_owned(), kind.declared_base().map(str::to_owned)))
1048                .collect()
1049        })
1050    }
1051
1052    /// One file holds a schema library's plugin and a declaration-only one,
1053    /// and each reaches its own generated file.
1054    #[test]
1055    fn mixed_plugins_generate() {
1056        let dir = tempfile::tempdir().expect("tempdir");
1057        let schema = missing_suffix(dir.path());
1058        let info = mixed_plug_info(dir.path());
1059        let out = dir.path().join("generated");
1060
1061        configure()
1062            .out_dir(&out)
1063            .schema(&schema)
1064            .plug_info(&info)
1065            .declaration_family("siteKinds")
1066            .generate()
1067            .expect("generates both");
1068
1069        let library = fs::read_to_string(out.join("testWarn.rs")).expect("the schema library is written");
1070        assert!(
1071            library.contains(r#"::openusd::kind::Decl::new("prop").base("component")"#),
1072            "{library}"
1073        );
1074        let family = fs::read_to_string(out.join("siteKinds.rs")).expect("the declaration family is written");
1075        assert!(
1076            family.contains(r#"::openusd::kind::Decl::new("chargroup").base("assembly")"#),
1077            "{family}"
1078        );
1079        assert!(
1080            family.contains(r#"::openusd::kind::Decl::new("site_root")"#),
1081            "{family}"
1082        );
1083        assert!(!family.contains("prop"), "{family}");
1084    }
1085
1086    /// A run with no schema at all still writes its declaration families.
1087    #[test]
1088    fn declarations_alone_generate() {
1089        let dir = tempfile::tempdir().expect("tempdir");
1090        let info = mixed_plug_info(dir.path());
1091        let out = dir.path().join("generated");
1092
1093        // Naming the file alone asks for nothing, as configuring no schema does.
1094        configure()
1095            .out_dir(&out)
1096            .plug_info(&info)
1097            .generate()
1098            .expect("nothing to do");
1099        assert!(!out.exists(), "nothing is written");
1100
1101        let error = configure()
1102            .out_dir(&out)
1103            .plug_info(&info)
1104            .declaration_family("siteKinds")
1105            .generate()
1106            .expect_err("testWarn has no schema library here");
1107        assert!(
1108            matches!(&error, Error::UnknownPlugin { plugin, .. } if plugin == "testWarn"),
1109            "{error}"
1110        );
1111
1112        let kinds = dir.path().join("kinds.json");
1113        fs::write(
1114            &kinds,
1115            r#"{"Plugins": [{"Name": "siteKinds", "Info": {"Kinds": {"chargroup": {"baseKind": "assembly"}}}}]}"#,
1116        )
1117        .expect("writes the plugInfo");
1118        configure()
1119            .out_dir(&out)
1120            .plug_info(&kinds)
1121            .declaration_family("siteKinds")
1122            .generate()
1123            .expect("generates the family");
1124        assert!(out.join("siteKinds.rs").exists());
1125    }
1126
1127    /// A declaration family carries its plugin's kinds and watches its file.
1128    #[test]
1129    fn declaration_family_kinds() {
1130        let dir = tempfile::tempdir().expect("tempdir");
1131        let info = mixed_plug_info(dir.path());
1132
1133        let output = configure()
1134            .plug_info(&info)
1135            .build_declarations("siteKinds")
1136            .expect("builds");
1137        assert_eq!(output.library_name(), "siteKinds");
1138        assert_eq!(output.layers(), [info]);
1139        assert_eq!(
1140            declared_kinds(&output),
1141            [
1142                ("chargroup".to_owned(), Some("assembly".to_owned())),
1143                ("site_root".to_owned(), None)
1144            ]
1145        );
1146        output.with_family(|family| assert!(family.schemas().is_empty()));
1147    }
1148
1149    /// A declaration family's name is its file's, so one that is no
1150    /// identifier is refused before anything is written.
1151    #[test]
1152    fn declaration_family_name_checked() {
1153        let dir = tempfile::tempdir().expect("tempdir");
1154        let out = dir.path().join("generated");
1155        for name in ["../kinds", "site/kinds", "my-kinds", ""] {
1156            let error = configure()
1157                .out_dir(&out)
1158                .plug_info(mixed_plug_info(dir.path()))
1159                .declaration_family(name)
1160                .generate()
1161                .expect_err("not an identifier");
1162            assert!(
1163                matches!(&error, Error::InvalidDeclarationFamily { family } if family == name),
1164                "{error}"
1165            );
1166        }
1167        let written = fs::read_dir(&out).expect("the output directory").count();
1168        assert_eq!(written, 0, "nothing is written");
1169        assert!(!dir.path().join("kinds.rs").exists(), "nor beside it");
1170    }
1171
1172    /// A declaration family no configured file declares a plugin for would
1173    /// write an empty file, so generation stops.
1174    #[test]
1175    fn unknown_declaration_family_refused() {
1176        let dir = tempfile::tempdir().expect("tempdir");
1177        let schema = missing_suffix(dir.path());
1178
1179        let error = configure()
1180            .out_dir(dir.path().join("generated"))
1181            .schema(&schema)
1182            .plug_info(mixed_plug_info(dir.path()))
1183            .declaration_family("siteKindz")
1184            .generate()
1185            .expect_err("no plugin is called siteKindz");
1186        assert!(
1187            matches!(&error, Error::UnknownDeclarationFamily { family } if family == "siteKindz"),
1188            "{error}"
1189        );
1190    }
1191
1192    /// One kind declared by two plugins would be refused by the registry, so
1193    /// generation refuses it first.
1194    #[test]
1195    fn duplicate_kind_refused() {
1196        let dir = tempfile::tempdir().expect("tempdir");
1197        let schema = missing_suffix(dir.path());
1198        let info = dir.path().join("twice.json");
1199        fs::write(
1200            &info,
1201            r#"{"Plugins": [
1202                {"Name": "testWarn", "Info": {"Kinds": {"prop": {"baseKind": "component"}}}},
1203                {"Name": "siteKinds", "Info": {"Kinds": {"prop": {}}}}
1204            ]}"#,
1205        )
1206        .expect("writes the plugInfo");
1207
1208        let error = configure()
1209            .out_dir(dir.path().join("generated"))
1210            .schema(&schema)
1211            .plug_info(&info)
1212            .declaration_family("siteKinds")
1213            .generate()
1214            .expect_err("both declare prop");
1215        assert!(
1216            matches!(&error, Error::DuplicateKind { kind, .. } if kind == "prop"),
1217            "{error}"
1218        );
1219    }
1220
1221    /// A plugin named after no library this run builds declares fields that
1222    /// would reach nothing, so generation stops.
1223    #[test]
1224    fn unknown_plugin_refused() {
1225        let dir = tempfile::tempdir().expect("tempdir");
1226        let schema = missing_suffix(dir.path());
1227        let info = plug_info(dir.path(), "testElsewhere", "role");
1228
1229        let error = configure()
1230            .out_dir(dir.path().join("generated"))
1231            .schema(&schema)
1232            .plug_info(&info)
1233            .generate()
1234            .expect_err("no library is called testElsewhere");
1235        assert!(
1236            matches!(&error, Error::UnknownPlugin { plugin, path } if plugin == "testElsewhere" && path == &info),
1237            "{error}"
1238        );
1239    }
1240
1241    /// A plugin's fields reach the library named after it: its schema data
1242    /// registers them, and the file is watched with the layers it read.
1243    #[test]
1244    fn plugin_fields_reach_library() {
1245        let dir = tempfile::tempdir().expect("tempdir");
1246        let schema = missing_suffix(dir.path());
1247        let info = plug_info(dir.path(), "testWarn", "role");
1248
1249        let output = configure()
1250            .plug_info(&info)
1251            .build_library(&schema, Views::Generate)
1252            .expect("builds");
1253        assert!(output.layers().contains(&info), "{:?}", output.layers());
1254        let names: Vec<String> = output.with_family(|family| {
1255            family
1256                .declared_metadata()
1257                .iter()
1258                .map(|field| field.name().to_owned())
1259                .collect()
1260        });
1261        assert_eq!(names, vec!["role"]);
1262    }
1263
1264    /// A list default is written into the declaration table as the value it
1265    /// decodes to, array and tuple alike.
1266    #[test]
1267    fn list_defaults_emitted() {
1268        let dir = tempfile::tempdir().expect("tempdir");
1269        let schema = missing_suffix(dir.path());
1270        let info = dir.path().join("plugInfo.json");
1271        fs::write(
1272            &info,
1273            r#"{"Plugins": [{"Name": "testWarn", "Info": {"SdfMetadata": {
1274                "weights": {"type": "double[]", "default": [0.0, 1.0], "appliesTo": "layers"},
1275                "up": {"type": "float3", "default": [0, 1, 0], "appliesTo": "layers"}
1276            }}}]}"#,
1277        )
1278        .expect("writes the plugInfo");
1279
1280        let output = configure()
1281            .plug_info(&info)
1282            .build_library(&schema, Views::Generate)
1283            .expect("builds");
1284        let fallbacks: Vec<Option<sdf::Value>> = output.with_family(|family| {
1285            family
1286                .declared_metadata()
1287                .iter()
1288                .map(usd::MetadataDecl::declared_fallback)
1289                .collect()
1290        });
1291        assert!(
1292            fallbacks.contains(&Some(sdf::Value::DoubleVec(vec![0.0, 1.0]))),
1293            "{fallbacks:?}"
1294        );
1295        assert!(output.rust.contains(".fallback(||"), "{}", output.rust);
1296    }
1297
1298    /// One field declared by two libraries would be refused by the registry,
1299    /// so generation refuses it first.
1300    #[test]
1301    fn duplicate_field_refused() {
1302        let dir = tempfile::tempdir().expect("tempdir");
1303        let first = missing_suffix(dir.path());
1304        let second_dir = dir.path().join("other");
1305        fs::create_dir(&second_dir).expect("creates a schema directory");
1306        let second = second_dir.join("schema.usda");
1307        fs::write(&second, MISSING_SUFFIX.replace("testWarn", "testOther")).expect("writes the schema");
1308
1309        let error = configure()
1310            .out_dir(dir.path().join("generated"))
1311            .schema(&first)
1312            .schema(&second)
1313            .plug_info(plug_info(dir.path(), "testWarn", "role"))
1314            .plug_info(plug_info(dir.path(), "testOther", "role"))
1315            .generate()
1316            .expect_err("both declare role");
1317        assert!(
1318            matches!(&error, Error::DuplicateMetadataField { field, .. } if field == "role"),
1319            "{error}"
1320        );
1321    }
1322
1323    /// Every layer a node library composes from is watched, a sublayer
1324    /// included, so an edit to any of them regenerates.
1325    #[test]
1326    fn shader_def_sublayers_watched() {
1327        let dir = tempfile::tempdir().expect("tempdir");
1328        let base = dir.path().join("base.usda");
1329        fs::write(
1330            &base,
1331            "#usda 1.0\n\ndef Shader \"UsdBase\"\n{\n    uniform token info:id = \"UsdBase\"\n}\n",
1332        )
1333        .expect("writes the sublayer");
1334        let root = dir.path().join("shaderDefs.usda");
1335        fs::write(&root, "#usda 1.0\n(\n    subLayers = [@./base.usda@]\n)\n").expect("writes the root");
1336
1337        let output = configure()
1338            .extern_library("usdShade", "crate::shade")
1339            .build_shader_defs("testNodes", &root)
1340            .expect("generates");
1341        let watched: Vec<_> = output
1342            .layers()
1343            .iter()
1344            .map(|layer| layer.file_name().expect("a file").to_owned())
1345            .collect();
1346        assert_eq!(watched, vec!["shaderDefs.usda", "base.usda"]);
1347        assert!(output.rust.contains("pub struct Base("), "{}", output.rust);
1348    }
1349
1350    /// A node taking its ports from a referenced layer watches that layer
1351    /// too, since the generated interface is read from it.
1352    #[test]
1353    fn shader_def_references_watched() {
1354        let dir = tempfile::tempdir().expect("tempdir");
1355        fs::write(
1356            dir.path().join("base.usda"),
1357            "#usda 1.0\n\ndef Shader \"Base\"\n{\n    float inputs:gain = 1\n}\n",
1358        )
1359        .expect("writes the referenced layer");
1360        let root = dir.path().join("shaderDefs.usda");
1361        fs::write(
1362            &root,
1363            "#usda 1.0\n\ndef Shader \"UsdGain\" (\n    references = @./base.usda@</Base>\n)\n{\n    uniform token info:id = \"UsdGain\"\n}\n",
1364        )
1365        .expect("writes the root");
1366
1367        let output = configure()
1368            .extern_library("usdShade", "crate::shade")
1369            .build_shader_defs("testNodes", &root)
1370            .expect("generates");
1371        assert!(output.rust.contains("pub fn gain_input("), "{}", output.rust);
1372        let watched: Vec<_> = output
1373            .layers()
1374            .iter()
1375            .map(|layer| layer.file_name().expect("a file").to_owned())
1376            .collect();
1377        assert_eq!(watched, vec!["shaderDefs.usda", "base.usda"]);
1378    }
1379
1380    /// A node referencing a layer that is not there fails generation rather
1381    /// than emitting the interface without what the layer would have given it.
1382    #[test]
1383    fn shader_def_missing_reference_refused() {
1384        let dir = tempfile::tempdir().expect("tempdir");
1385        let root = dir.path().join("shaderDefs.usda");
1386        fs::write(
1387            &root,
1388            "#usda 1.0\n\ndef Shader \"UsdGain\" (\n    references = @./missing.usda@</Base>\n)\n{\n    uniform token info:id = \"UsdGain\"\n}\n",
1389        )
1390        .expect("writes the root");
1391
1392        let error = configure()
1393            .extern_library("usdShade", "crate::shade")
1394            .build_shader_defs("testNodes", &root)
1395            .expect_err("the reference resolves to nothing");
1396        assert!(matches!(error, Error::Composition { .. }), "{error}");
1397    }
1398
1399    /// A run configuring only shader definitions still generates them.
1400    #[test]
1401    fn shader_defs_alone_generated() {
1402        let dir = tempfile::tempdir().expect("tempdir");
1403        let out = dir.path().join("generated");
1404        let defs = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/nodes/shaderDefs.usda");
1405
1406        configure()
1407            .out_dir(&out)
1408            .extern_library("usdShade", "crate::shade")
1409            .shader_defs("tinyNodes", &defs)
1410            .generate()
1411            .expect("generates");
1412        assert!(out.join("tinyNodes.rs").is_file(), "the node library is written");
1413    }
1414
1415    /// With schemas to build and nowhere to put them, generation stops rather
1416    /// than guessing.
1417    #[test]
1418    fn missing_out_dir_reported() {
1419        let error = configure()
1420            .schema("schemas/usdGeom/schema.usda")
1421            .generate()
1422            .expect_err("this crate has no build script, so its tests run without OUT_DIR set");
1423
1424        assert!(matches!(error, Error::NoOutDir), "{error}");
1425    }
1426
1427    /// A repeated option accumulates, except an extern library, which is keyed
1428    /// by library name and keeps the last declaration.
1429    ///
1430    /// Nothing reads either yet; they are what the generator will be given.
1431    #[test]
1432    fn repeated_options() {
1433        let builder = configure()
1434            .schema("schemas/usdGeom/schema.usda")
1435            .schema("schemas/usdLux/schema.usda")
1436            .extern_library("usdGeom", "crate::geom")
1437            .extern_library("usdGeom", "openusd_schemas::geom");
1438
1439        assert_eq!(builder.schemas.len(), 2);
1440        assert_eq!(
1441            builder.extern_libraries.get("usdGeom").map(String::as_str),
1442            Some("openusd_schemas::geom")
1443        );
1444    }
1445}