Skip to main content

htl_core/
build_target.rs

1//! [`BuildTarget`]: what runs the output of an htl project.
2//!
3//! One enum, three entries, and everything else about a target derived from it: the crate
4//! types cargo is told to build ([`BuildTarget::crate_types`]), whether the project has an
5//! entry script ([`BuildTarget::entry`]), and the sentence that names the thing on the far
6//! end ([`BuildTarget::runs_it`]). `htl.toml` records the choice as `[build] target`, and
7//! the scaffolder in `htl-cli` is a consumer of this type rather than the place it is
8//! defined.
9
10use serde::{Deserialize, Deserializer};
11use std::fmt;
12use std::str::FromStr;
13
14/// **A build target is what runs htl's output.** The `htl` binary runs a `.hb` bundle; the
15/// OS runs a native binary; a caller written in C, Python or C# loads a `cdylib`. That is
16/// the axis, and it is the only thing the entries below differ about — README
17/// "[Build targets](https://github.com/ynishi/htl#build-targets---target-name)" has the
18/// table of what each one produces.
19///
20/// # What is in the enum, and what is derived from it
21///
22/// The enum is over *what runs the output*, and nothing else. Cargo's `[lib] crate-type`
23/// and the entry-script rule are answers derived from an entry ([`crate_types`],
24/// [`entry`]), not fields stored beside it, so adding a target is adding one arm and the
25/// answers it gives rather than a row of parallel data that can disagree with itself. A
26/// platform or a target triple is deliberately not in here: `bin` on Linux and `bin` on
27/// Windows are the same build target, and if a platform ever has to be named it is an
28/// attribute of one target, not a fourth entry.
29///
30/// [`crate_types`]: BuildTarget::crate_types
31/// [`entry`]: BuildTarget::entry
32///
33/// # Why it is not called a host
34///
35/// Two of the three entries happen to be Rust crates, which is why this used to be called
36/// a host; an output nothing Rust runs — a `.love` bundle, say — is the entry that makes
37/// that word plainly wrong.
38///
39/// *Host* keeps its own meaning throughout htl and is not this: it is the Rust side that
40/// embeds the Lua state — `#[host_module]`, the `src/host.d.tl` generated from it, and
41/// `[build] host` / `htl build --host x,y`, which name the modules that side provides at
42/// run time. A project can have a host and the default target (`htl build` alone), and two
43/// targets can share one host, so they are two axes rather than two words for one.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum BuildTarget {
46    /// A `.hb` bundle, run by the `htl` binary. What plain `htl build` produces, and what
47    /// every project without Rust in it is.
48    Hb,
49    /// A native binary, run by the OS: a library crate holding the host module and the
50    /// embedded scripts, with a thin binary on top.
51    Bin,
52    /// A C ABI library, loaded by a caller that is not written in Rust.
53    Cdylib,
54}
55
56impl BuildTarget {
57    /// Every target there is, in the order they are offered and reported.
58    pub const ALL: &'static [BuildTarget] =
59        &[BuildTarget::Hb, BuildTarget::Bin, BuildTarget::Cdylib];
60
61    /// How it is spelled on the command line (`--target <name>`) and in `htl.toml`.
62    pub fn name(&self) -> &'static str {
63        match self {
64            BuildTarget::Hb => "hb",
65            BuildTarget::Bin => "bin",
66            BuildTarget::Cdylib => "cdylib",
67        }
68    }
69
70    /// Every name, in [`ALL`](BuildTarget::ALL) order: what a flag offers and what a typo
71    /// is answered with.
72    pub fn names() -> Vec<&'static str> {
73        BuildTarget::ALL.iter().map(BuildTarget::name).collect()
74    }
75
76    /// `[lib] crate-type = [...]` beyond the default `rlib`, which needs no section at all.
77    /// Derived rather than stored: the crate shape is a consequence of what loads the
78    /// output, so it is answered here and `Cargo.toml` has nothing else to know about it.
79    pub fn crate_types(&self) -> &'static [&'static str] {
80        match self {
81            // No Rust crate at all; `htl build` writes the bundle.
82            BuildTarget::Hb => &[],
83            // The default `rlib`, plus the binary cargo builds from `src/main.rs`.
84            BuildTarget::Bin => &[],
85            // The shared object a caller loads, and the static library Unity on iOS links;
86            // the second costs one more artefact and nothing else.
87            BuildTarget::Cdylib => &["rlib", "cdylib", "staticlib"],
88        }
89    }
90
91    /// What this target has to say about `src/main.tl`.
92    pub fn entry(&self) -> Script {
93        match self {
94            BuildTarget::Hb => Script::Either,
95            BuildTarget::Bin => Script::Either,
96            // A `cdylib` has no entry point of its own, and the caller that loads it brings
97            // its own `main`.
98            BuildTarget::Cdylib => Script::Forbids,
99        }
100    }
101
102    /// Who is on the far end, in the words the README's table and `htl new`'s refusals use.
103    pub fn runs_it(&self) -> &'static str {
104        match self {
105            BuildTarget::Hb => "the htl binary",
106            BuildTarget::Bin => "the OS, as a binary",
107            BuildTarget::Cdylib => "a C / Python / Unity caller",
108        }
109    }
110}
111
112impl fmt::Display for BuildTarget {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(self.name())
115    }
116}
117
118impl FromStr for BuildTarget {
119    type Err = String;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        BuildTarget::ALL
123            .iter()
124            .copied()
125            .find(|t| t.name() == s)
126            .ok_or_else(|| {
127                format!(
128                    "unknown target `{s}`; registered targets: {}",
129                    BuildTarget::names().join(", ")
130                )
131            })
132    }
133}
134
135/// The string form is the only form: `target = "cdylib"` in `htl.toml` goes through
136/// [`FromStr`], so a name that is not one is refused with the same sentence the command
137/// line answers a typo with, rather than with serde's list of variant spellings.
138impl<'de> Deserialize<'de> for BuildTarget {
139    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
140        let s = String::deserialize(d)?;
141        s.parse().map_err(serde::de::Error::custom)
142    }
143}
144
145/// What a target has to say about `src/main.tl`. `--lib` is the user's side of the same
146/// question, and the two are reconciled once, where the scaffold resolves a target, before
147/// anything is written.
148// `Requires` is the half no target has yet — #104's window loop is the one that will — so
149// until then the code that reads it is exercised by this module's tests.
150#[derive(Debug, PartialEq, Eq, Clone, Copy)]
151pub enum Script {
152    /// The target runs an entry script and cannot be built without one (a window loop).
153    Requires,
154    /// The target is a library for someone else to call and has no entry point (a C ABI).
155    Forbids,
156    /// Either shape works; `--lib` decides.
157    Either,
158}
159
160impl Script {
161    /// Does this rule accept a project built with `--lib` (`lib = true`), or without one?
162    pub fn accepts(self, lib: bool) -> bool {
163        match self {
164            Script::Requires => !lib,
165            Script::Forbids => lib,
166            Script::Either => true,
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::{BuildTarget, Script};
174
175    /// The name is the only spelling there is, and it round-trips: what `htl.toml` records
176    /// is what `--target` takes, for every entry, with nothing spelled twice.
177    #[test]
178    fn every_target_round_trips_through_its_name() {
179        for t in BuildTarget::ALL {
180            assert_eq!(t.name().parse::<BuildTarget>().unwrap(), *t);
181            assert_eq!(t.to_string(), t.name());
182        }
183        assert_eq!(BuildTarget::names(), vec!["hb", "bin", "cdylib"]);
184    }
185
186    /// A name that is not one is answered with the ones that are — the same sentence
187    /// whether it arrived on the command line or in `htl.toml`.
188    #[test]
189    fn an_unknown_name_is_refused_with_all_three() {
190        let e = "rust".parse::<BuildTarget>().unwrap_err();
191        assert!(e.contains("unknown target `rust`"), "{e}");
192        for n in BuildTarget::names() {
193            assert!(e.contains(n), "{e}");
194        }
195    }
196
197    /// The crate shape follows from what loads the output: only the C ABI target needs a
198    /// `[lib]` section, and it needs all three of those types.
199    #[test]
200    fn crate_types_are_derived_from_the_target() {
201        assert!(BuildTarget::Hb.crate_types().is_empty());
202        assert!(BuildTarget::Bin.crate_types().is_empty());
203        assert_eq!(
204            BuildTarget::Cdylib.crate_types(),
205            ["rlib", "cdylib", "staticlib"]
206        );
207    }
208
209    /// So does the entry-script rule: a `cdylib` refuses one, and the other two leave the
210    /// question to `--lib`.
211    #[test]
212    fn the_entry_rule_is_derived_from_the_target() {
213        assert_eq!(BuildTarget::Hb.entry(), Script::Either);
214        assert_eq!(BuildTarget::Bin.entry(), Script::Either);
215        assert_eq!(BuildTarget::Cdylib.entry(), Script::Forbids);
216
217        assert!(Script::Requires.accepts(false) && !Script::Requires.accepts(true));
218        assert!(Script::Forbids.accepts(true) && !Script::Forbids.accepts(false));
219        assert!(Script::Either.accepts(true) && Script::Either.accepts(false));
220    }
221}