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}