bern_target_api/lib.rs
1//! The interface every UI target module implements.
2//!
3//! A target describes itself and its capabilities, checks the shared IR,
4//! generates code and resources, and returns a build plan that the build
5//! driver executes. The compiler core only talks to this interface; it never
6//! imports a concrete target, and disabling one target leaves the others
7//! buildable.
8
9pub mod brand_assets;
10pub mod glue;
11pub mod icon;
12pub mod icon_set;
13pub mod names;
14pub mod skill;
15pub mod style;
16pub mod svg;
17pub mod texts;
18
19mod check;
20mod plan;
21mod report;
22
23pub use check::check_common;
24pub use plan::{
25 BuildPlan, BuildStep, Run, SimulatedSystem, archive, dist_dir, files_below,
26 package_architecture,
27};
28pub use report::{FeatureReport, feature_report};
29
30use bern_ir::{Application, IR_VERSION, SourceLocation};
31use std::fmt;
32use std::ops::RangeInclusive;
33use std::path::PathBuf;
34
35/// Where a target's colors come from.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum ColorSource {
38 /// The UI framework's own light/dark system and standard styles.
39 Native,
40 /// Colors generated by the Bern theme algorithm.
41 BernTheme,
42}
43
44/// How a target can present `Window` declarations.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct WindowCapabilities {
47 pub native: bool,
48 pub virtual_windows: bool,
49 /// Draggable, resizable windows: native windows, or floating panels.
50 pub floating: bool,
51}
52
53/// The media backend and video presenter a target assembles for `Video`.
54/// The two are versioned separately from the target and from each other.
55#[derive(Clone, Debug)]
56pub struct MediaSupport {
57 /// Versions of the media contract both parts implement.
58 pub contract_versions: RangeInclusive<u32>,
59 /// Media backend that loads and plays, for example `avfoundation`.
60 pub backend: &'static str,
61 pub backend_version: &'static str,
62 /// Presenter that attaches players to views, for example `avplayerview`.
63 pub presenter: &'static str,
64 pub presenter_version: &'static str,
65 /// Players keep playing while no view is attached.
66 pub background_playback: bool,
67 /// Several views may show one player at the same time.
68 pub multi_view: bool,
69}
70
71#[derive(Clone, Debug)]
72pub struct TargetDescriptor {
73 /// Stable target id, for example `swiftui-macos`.
74 pub id: &'static str,
75 /// UI technology, for example `swiftui`.
76 pub ui: &'static str,
77 pub platforms: &'static [&'static str],
78 pub version: &'static str,
79 /// IR versions this target consumes.
80 pub ir_versions: RangeInclusive<u32>,
81 pub color_source: ColorSource,
82 /// Built-in components this target can realize.
83 pub components: &'static [&'static str],
84 pub windows: WindowCapabilities,
85 /// Entry channels the target's host can assemble besides the UI.
86 pub channels: &'static [&'static str],
87 /// Media support for `Video` and application players, if any.
88 pub media: Option<MediaSupport>,
89 /// Style overrides the target can show; none by default.
90 pub styles: &'static [StyleSupport],
91 /// Motion properties the target shows (`appear`, `disappear`,
92 /// `reflow`, `hero`); the others change without motion.
93 pub motion: &'static [&'static str],
94 /// Whether windows honor `chrome`, `immersive` and `idle_after`.
95 pub window_chrome: bool,
96 /// Whether `Text(mark:)` draws its highlight; otherwise the text shows
97 /// plain.
98 pub text_marks: bool,
99 /// Whether items of `Sidebar`, `Tabs` and `Toolbar` may come and go
100 /// with `if`, `if let`, `match` and `ForEach`.
101 pub dynamic_items: bool,
102 /// Further parts of the IR the target builds, named in [`feature`];
103 /// using one a target does not list fails the build or, where a plain
104 /// form is shown instead, warns.
105 pub features: &'static [&'static str],
106}
107
108/// Names of IR features a target lists in [`TargetDescriptor::features`].
109pub mod feature {
110 /// `Button` with a child that it shows instead of its label.
111 pub const BUTTON_CONTENT: &str = "button_content";
112 /// `reveal: hover`: content shown while the pointer is over its
113 /// container; without it the content always shows.
114 pub const REVEAL: &str = "reveal";
115 /// `Search` in a `Toolbar`.
116 pub const TOOLBAR_SEARCH: &str = "toolbar_search";
117 /// `Search` with children: results shown below the field.
118 pub const SEARCH_RESULTS: &str = "search_results";
119 /// Actions that change a session draft: `draft.panel.set(true)`.
120 pub const SESSION_ACTIONS: &str = "session_actions";
121 /// `Switch(style: button)`, and a `Switch` in a `Toolbar`.
122 pub const TOGGLE_BUTTON: &str = "toggle_button";
123 /// `Window(role: dialog)` shown modally over the primary window;
124 /// without it a dialog is a window of its own.
125 pub const DIALOG_WINDOWS: &str = "dialog_windows";
126 /// Lengths bound to expressions, such as `padding: if window.idle { 0 }
127 /// else { 24 }`, sizes of `Column`, `Row` and `Stack`, and the
128 /// `corner` of a `Video`.
129 pub const BOUND_LENGTHS: &str = "bound_lengths";
130 /// `ScrollView(anchor:)`: the item of `position` at a fraction of the
131 /// view without snapping, as near as the content reaches.
132 pub const SCROLL_ANCHOR: &str = "scroll_anchor";
133 /// `ScrollView(browsing:)`: a flag that holds while the person scrolls
134 /// by hand.
135 pub const SCROLL_BROWSING: &str = "scroll_browsing";
136 /// `blur` bound to state, changing with `animate`.
137 pub const BOUND_BLUR: &str = "bound_blur";
138 /// `Text(bold:)`: a text bold while a condition holds.
139 pub const TEXT_BOLD: &str = "text_bold";
140 /// `appearance: dark` or `light` on a part of the interface; without it
141 /// the part follows the window's appearance.
142 pub const APPEARANCE: &str = "appearance";
143 /// Keyboard `shortcut`s of buttons, menu items, switches and search
144 /// fields; without them the controls work from the pointer only.
145 pub const SHORTCUTS: &str = "shortcuts";
146 /// Shortcuts that follow state, such as `shortcut: app.keys.open` and
147 /// `Shortcut(key: app.keys.back)`: the runtime reads the text each time
148 /// it changes; without it a shortcut is a literal.
149 pub const BOUND_SHORTCUTS: &str = "bound_shortcuts";
150 /// `ShortcutInput`: a button that takes the next key pressed, with its
151 /// modifiers, as a shortcut.
152 pub const SHORTCUT_INPUT: &str = "shortcut_input";
153 /// `MenuBar` in the `WindowSet`: the application's menus in the system
154 /// menu bar. A target whose platform has no menu bar for applications
155 /// lists it as well: there the menus show nowhere, `window.menu_bar`
156 /// is false, and nothing is left to warn about.
157 pub const MENU_BAR: &str = "menu_bar";
158 /// `StatusItem` in the `WindowSet`: the application's item in the
159 /// system's status area, with its menu, and the application running on
160 /// after its last window closes. A target whose platform has no status
161 /// area for applications lists it as well: there the item shows nowhere
162 /// and nothing is left to warn about.
163 pub const STATUS_ITEM: &str = "status_item";
164 /// `WindowSet(accent:, appearance:)` chosen at run time; without it the
165 /// windows keep the system's accent and appearance.
166 pub const RUNTIME_THEME: &str = "runtime_theme";
167 pub const RUNTIME_PALETTE: &str = "runtime_palette";
168 /// `Image(source:)` from a `MediaSource`: a file or an http(s) URL.
169 pub const RUNTIME_IMAGES: &str = "runtime_images";
170 /// `Shape(fill_from:)`, the average color of a picture, and `fade`.
171 pub const PICTURE_FILLS: &str = "picture_fills";
172 /// `Image(fit: cover)` without `width`, `height` or `aspect`: the
173 /// picture covers the room its container gives, cropped.
174 pub const COVER_IMAGES: &str = "cover_images";
175 /// `pick:` on `Button`, `IconButton` and `MenuItem`: the system's open
176 /// panel asks for a file before the action runs.
177 pub const FILE_PICK: &str = "file_pick";
178 /// `Table`: rows of data in columns, with a selection of the rows'
179 /// keys, a row menu, a primary action and sorting by a heading.
180 pub const TABLE: &str = "table";
181 /// `TextInput(current:)`: a field showing engine state, each edit
182 /// running its action, the user's text held while being edited.
183 pub const CONTROLLED_TEXT: &str = "controlled_text";
184 /// `Watch`: a command runs when the element is mounted and whenever
185 /// the value it follows changes.
186 pub const WATCH: &str = "watch";
187 /// `Switch(login_item: true)`: whether the system opens the
188 /// application at login, shown where it can, left out elsewhere.
189 pub const LOGIN_SWITCH: &str = "login_switch";
190 /// `paste:` on `Button`, `IconButton`, `MenuItem` and `CommandItem`:
191 /// the clipboard's text goes into a draft before the action runs.
192 pub const PASTE: &str = "paste";
193 /// `ScrollView(snap:, position:)`: scrolling comes to rest on an item,
194 /// and a session field follows and sets the item it stands at.
195 pub const SCROLL_SNAP: &str = "scroll_snap";
196 /// `Rail`: elements placed at fractions of a strip's length.
197 pub const RAILS: &str = "rails";
198 /// `PopoverButton`: a button that shows its children in a popover.
199 pub const POPOVER_BUTTON: &str = "popover_button";
200 /// `text { large_title: heavy }` in `style!`: weights of text styles.
201 pub const TEXT_WEIGHTS: &str = "text_weights";
202 /// `Text(lines:)` and `Text(line_height:)`: text limited to a number of
203 /// lines, or with lines of a given height.
204 pub const TEXT_LINES: &str = "text_lines";
205 /// `Image(corner:)`: an image with rounded corners.
206 pub const IMAGE_CORNERS: &str = "image_corners";
207 /// `Sidebar(width:)`: a sidebar of a fixed width; without it the
208 /// sidebar keeps the platform's width.
209 pub const SIDEBAR_WIDTH: &str = "sidebar_width";
210 /// `Switch(checked:)`, `Picker(selected:)`, `Slider(current:)` and `PopoverButton(open:)`:
211 /// controls showing engine state and reporting changes as commands.
212 pub const CONTROLLED_VALUES: &str = "controlled_values";
213 /// `Search(style: palette)` and its `SearchItem`s: a toolbar search
214 /// field that grows into a results panel.
215 pub const SEARCH_PALETTE: &str = "search_palette";
216 /// `Image(rendering: template)`: the image's alpha takes the text color.
217 pub const IMAGE_TEMPLATE: &str = "image_template";
218 /// `Image(aspect:)`, `Grid(row_gap:)` and `Slider(label_hidden:)`.
219 pub const ADAPTIVE_IMAGES: &str = "adaptive_images";
220 /// `ScaleBox`, `Positioned` and `Marquee`: a fixed coordinate scene
221 /// scaled to its room.
222 pub const SCALED_SCENE: &str = "scaled_scene";
223 /// `Accordion`: columns of which one is open at a time.
224 pub const ACCORDION_LAYOUT: &str = "accordion_layout";
225 /// `ControlGroup` and capsule-shaped system controls.
226 pub const CAPSULE_CONTROLS: &str = "capsule_controls";
227 /// `Modal`, `Flow`, `Timer`, `PickerOption` and `Stack(enabled:)`.
228 pub const MODAL_FLOW_TIMER: &str = "modal_flow_timer";
229 /// `SidebarItem(badge:)`: a live count beside a page.
230 pub const PAGE_BADGES: &str = "page_badges";
231 /// `Window(idle_enabled:)`: idle detection switched on and off by
232 /// state. Without it a target keeps detecting idleness all the time.
233 pub const IDLE_CONTROL: &str = "idle_control";
234 /// `Button(width: fill)`: a system button stretched across its
235 /// container, as a phone's main action.
236 pub const FILL_BUTTONS: &str = "fill_buttons";
237 /// `Grid(cell_ratio:, cell_extra:)`: rows as tall as the column width
238 /// gives, whatever their content.
239 pub const GRID_CELLS: &str = "grid_cells";
240 /// A `Search` standing in the page, not in a `Toolbar` or `Sidebar`:
241 /// the platform's search field with its results below it.
242 pub const PAGE_SEARCH: &str = "page_search";
243 /// `Section(style: plain)`: rows on the page itself, as a heading that
244 /// scrolls with its form; `Section(padding_x:, padding_y:)` its rows'
245 /// insets and `Form(gap:)` the room between its sections.
246 pub const FORM_LAYOUT: &str = "form_layout";
247 /// `Modal(cover: compact)`: the modal covers the whole window where it
248 /// is compact.
249 pub const MODAL_COVER: &str = "modal_cover";
250 /// `Window(landscape:)`: a phone turns the window to landscape while
251 /// it holds, and back to portrait. A target whose windows never turn
252 /// (a television, a desktop window) lists it as well: there the
253 /// property does not apply, and nothing is left to warn about.
254 pub const WINDOW_LANDSCAPE: &str = "window_landscape";
255 /// `window.short`: the window is shorter than the compact breakpoint.
256 pub const WINDOW_SHORT: &str = "window_short";
257 /// `TabAccessory`: a bar kept with the tabs over every page.
258 pub const TAB_ACCESSORY: &str = "tab_accessory";
259 /// Transitions growing along one axis from a fixed edge.
260 pub const EDGE_GROW: &str = "edge_grow";
261 /// An entrance after a delay, including the first mounted frame.
262 pub const DELAYED_APPEAR: &str = "delayed_appear";
263 /// A `Spacer` in a `Toolbar`, parting leading items from trailing ones,
264 /// and `Text` in a toolbar.
265 pub const TOOLBAR_LEADING: &str = "toolbar_leading";
266 /// `padding_x` and `padding_y`: different padding across and along.
267 pub const AXIS_PADDING: &str = "axis_padding";
268 /// `Menu(style: icon)`: a menu shown as a round icon button.
269 pub const ICON_MENUS: &str = "icon_menus";
270 /// `Label(display: icon)`: an icon with a nonvisual accessible title.
271 pub const ICON_LABELS: &str = "icon_labels";
272 /// `Tab(display: icon)`: a tab showing only its icon, its label the
273 /// accessible name.
274 pub const ICON_TABS: &str = "icon_tabs";
275 /// `Row(align: baseline)`.
276 pub const ROW_BASELINE: &str = "row_baseline";
277 /// `SidebarHeader`: content pinned above a sidebar's list.
278 pub const SIDEBAR_HEADER: &str = "sidebar_header";
279 /// `Detail(titled: false)`: a detail column that leaves its title bar
280 /// without the destination's name, for pages that show their own.
281 pub const UNTITLED_DETAIL: &str = "untitled_detail";
282 /// The `medium` and `large` layout sizes and `window.compact` and its
283 /// siblings in expressions.
284 pub const SIZE_CLASSES: &str = "size_classes";
285 /// Plural branches, dates and durations in translated texts, and enum
286 /// labels that show their variant's fields.
287 pub const TEXT_FORMATS: &str = "text_formats";
288}
289
290/// Whether the application writes numbers, dates or durations in its
291/// texts, or shows enum fields in labels: what [`feature::TEXT_FORMATS`]
292/// names.
293pub fn uses_text_formats(app: &Application) -> bool {
294 use bern_protocol::format::{Piece, parse};
295 let formatted = app.texts.iter().any(|text| {
296 text.translations.values().any(|translation| {
297 parse(translation).is_ok_and(|pieces| {
298 pieces
299 .iter()
300 .any(|piece| !matches!(piece, Piece::Text(_) | Piece::Value(_)))
301 })
302 })
303 });
304 let labels_show_fields = app.types.enums.iter().any(|def| {
305 def.variants.iter().any(|variant| {
306 variant
307 .label
308 .as_ref()
309 .is_some_and(|label| !label.params.is_empty())
310 })
311 });
312 formatted || labels_show_fields
313}
314
315/// The style effects a target shows for one control kind: outside any
316/// state, and in each state.
317#[derive(Clone, Copy, Debug)]
318pub struct StyleSupport {
319 pub component: &'static str,
320 pub base: &'static [&'static str],
321 pub states: &'static [(bern_ir::style::StyleState, &'static [&'static str])],
322}
323
324impl StyleSupport {
325 fn shows(&self, state: Option<bern_ir::style::StyleState>, effect: &str) -> bool {
326 match state {
327 None => self.base.contains(&effect),
328 Some(state) => self
329 .states
330 .iter()
331 .any(|(supported, effects)| *supported == state && effects.contains(&effect)),
332 }
333 }
334}
335
336impl TargetDescriptor {
337 pub fn supports_ir(&self) -> bool {
338 self.ir_versions.contains(&IR_VERSION)
339 }
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub enum Severity {
344 Error,
345 Warning,
346 /// Information a report must show, such as an exemption.
347 Note,
348}
349
350/// A diagnostic pointing back to the developer's declaration.
351#[derive(Clone, Debug, PartialEq, Eq)]
352pub struct Diagnostic {
353 pub severity: Severity,
354 pub code: Option<&'static str>,
355 pub message: String,
356 pub location: Option<SourceLocation>,
357}
358
359impl Diagnostic {
360 pub fn error(message: impl Into<String>) -> Self {
361 Self {
362 severity: Severity::Error,
363 code: None,
364 message: message.into(),
365 location: None,
366 }
367 }
368
369 pub fn warning(message: impl Into<String>) -> Self {
370 Self {
371 severity: Severity::Warning,
372 ..Self::error(message)
373 }
374 }
375
376 pub fn note(message: impl Into<String>) -> Self {
377 Self {
378 severity: Severity::Note,
379 ..Self::error(message)
380 }
381 }
382
383 pub fn code(mut self, code: &'static str) -> Self {
384 self.code = Some(code);
385 self
386 }
387
388 pub fn at(mut self, location: SourceLocation) -> Self {
389 self.location = Some(location);
390 self
391 }
392
393 pub fn is_error(&self) -> bool {
394 self.severity == Severity::Error
395 }
396}
397
398impl fmt::Display for Diagnostic {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 let level = match self.severity {
401 Severity::Error => "error",
402 Severity::Warning => "warning",
403 Severity::Note => "note",
404 };
405 match self.code {
406 Some(code) => write!(f, "{level}[{code}]: {}", self.message)?,
407 None => write!(f, "{level}: {}", self.message)?,
408 }
409 if let Some(location) = &self.location {
410 write!(
411 f,
412 "\n --> {}:{}:{}",
413 location.file, location.line, location.column
414 )?;
415 }
416 Ok(())
417 }
418}
419
420/// Target settings from `[targets.<name>]` in `Bern.toml`.
421#[derive(Clone, Debug, Default)]
422pub struct TargetConfig {
423 pub name: String,
424 pub platform: String,
425 pub ui: Option<String>,
426 /// Web build mode: `app` (engine in the page) or `ui` (separate engine).
427 pub web_mode: Option<String>,
428 /// Browser extension entry and optional build command (receives the output directory).
429 pub web_extension: Option<WebExtension>,
430 pub channels: Vec<String>,
431 /// Windows front ends to build, from [`WINDOWS_FRONTENDS`]; both unless
432 /// the target names fewer. Empty for other platforms.
433 pub frontends: Vec<String>,
434 /// Override the target's web document capability. False removes all
435 /// WebView dependencies and compiles SVGs into static pictures.
436 pub webview: Option<bool>,
437 /// Assemble the portable JavaScript host (off unless requested).
438 pub javascript: bool,
439 /// Explicit Android host capabilities; absent on other platforms.
440 pub android: Option<AndroidOptions>,
441}
442
443/// Android capabilities needed by an application's Rust engine, which
444/// cannot be inferred from its UI. Multicast also enables network access.
445#[derive(Clone, Debug, Default, serde::Deserialize)]
446#[serde(default, deny_unknown_fields)]
447pub struct AndroidOptions {
448 pub network: bool,
449 pub multicast: bool,
450}
451
452impl TargetConfig {
453 pub fn has_webview(&self) -> bool {
454 self.platform == "browser"
455 || (self.platform != "tvos"
456 && self.webview.unwrap_or(matches!(
457 self.platform.as_str(),
458 "macos" | "ios" | "android" | "windows" | "linux" | "browser"
459 )))
460 }
461
462 pub fn javascript_backend(&self) -> Option<&'static str> {
463 self.javascript.then(|| {
464 if self.has_webview() {
465 if self.platform == "browser" {
466 "browser"
467 } else {
468 "webview"
469 }
470 } else {
471 "boa"
472 }
473 })
474 }
475}
476
477/// An application-owned browser adapter. Its `index.js` exports a setup function.
478#[derive(Clone, Debug, serde::Deserialize)]
479#[serde(deny_unknown_fields)]
480pub struct WebExtension {
481 pub path: String,
482 #[serde(default)]
483 pub build: Vec<String>,
484}
485
486/// The Windows front ends: WinUI 3 on the Windows App Runtime, and the
487/// system XAML through XAML Islands, which every Windows 10 and 11 has.
488pub const WINDOWS_FRONTENDS: [&str; 2] = ["winui3", "xaml-islands"];
489
490/// Where the Bern crates come from: the source of the project's own `bern`
491/// dependency. Generated crates take every Bern crate from it, so the
492/// application and the code generated around it share one copy of each.
493#[derive(Clone, Debug, PartialEq, Eq)]
494pub enum Framework {
495 /// A checkout whose `crates` directory holds every Bern crate.
496 Path(PathBuf),
497 /// A Git repository, at the reference the project names, if any:
498 /// `("branch", …)`, `("tag", …)` or `("rev", …)`.
499 Git {
500 url: String,
501 reference: Option<(String, String)>,
502 },
503 /// A release on crates.io, with the project's version requirement.
504 Registry(String),
505}
506
507impl Framework {
508 /// The dependency on the Bern crate `name`, as the inside of an inline
509 /// table, for example `path = "/src/bern/crates/bern-ffi"`.
510 pub fn dependency(&self, name: &str) -> String {
511 match self {
512 Framework::Path(crates) => {
513 let path = crates.join(name).display().to_string();
514 format!("path = {}", toml_string(&path.replace('\\', "/")))
515 }
516 Framework::Git { url, reference } => {
517 let mut out = format!("git = {}", toml_string(url));
518 if let Some((kind, value)) = reference {
519 out.push_str(&format!(", {kind} = {}", toml_string(value)));
520 }
521 out
522 }
523 Framework::Registry(version) => format!("version = {}", toml_string(version)),
524 }
525 }
526}
527
528/// A TOML basic string.
529pub fn toml_string(text: &str) -> String {
530 let mut out = String::from("\"");
531 for c in text.chars() {
532 match c {
533 '"' => out.push_str("\\\""),
534 '\\' => out.push_str("\\\\"),
535 c if c.is_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
536 c => out.push(c),
537 }
538 }
539 out.push('"');
540 out
541}
542
543/// Paths and packages a target needs to plan its build.
544#[derive(Clone, Debug)]
545pub struct BuildContext {
546 /// Directory containing `Bern.toml`.
547 pub project_dir: PathBuf,
548 /// Cargo workspace root.
549 pub workspace_dir: PathBuf,
550 /// `target/bern/<target-name>/`, owned by this target.
551 pub output_dir: PathBuf,
552 pub target: TargetConfig,
553 pub protocol_package: String,
554 pub protocol_path: PathBuf,
555 /// Rust path of the protocol module, for example `showcase_protocol::showcase`.
556 pub protocol_module: String,
557 pub engine_package: String,
558 pub engine_path: PathBuf,
559 /// Rust path of the engine type, which implements `Default`.
560 pub engine_type: String,
561 /// The application's modules, in catalog order.
562 pub modules: Vec<ModuleBuild>,
563 /// Where generated crates take the Bern crates from.
564 pub framework: Framework,
565 pub release: bool,
566}
567
568/// The crates of one application module, for the generated glue.
569#[derive(Clone, Debug)]
570pub struct ModuleBuild {
571 pub id: String,
572 pub protocol_package: String,
573 pub protocol_path: PathBuf,
574 /// Rust path of the module's protocol module, e.g. `downloads_protocol::downloads`.
575 pub protocol_module: String,
576 pub engine_package: String,
577 pub engine_path: PathBuf,
578 /// Rust path of the module engine type, which implements `Default`.
579 pub engine_type: String,
580 /// Whether the module can be switched off at run time.
581 pub optional: bool,
582 /// Whether an optional module runs before the application saved a choice.
583 pub enabled: bool,
584 pub depends: Vec<String>,
585}
586
587#[derive(Clone, Debug, PartialEq, Eq)]
588pub struct GeneratedFile {
589 /// Path relative to the output directory.
590 pub path: PathBuf,
591 pub contents: Vec<u8>,
592}
593
594impl GeneratedFile {
595 pub fn text(path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
596 Self {
597 path: path.into(),
598 contents: contents.into().into_bytes(),
599 }
600 }
601}
602
603/// A UI target module.
604pub trait Target: Send + Sync {
605 fn descriptor(&self) -> TargetDescriptor;
606
607 /// Operating systems that can build this target, as
608 /// `std::env::consts::OS` names them; empty when any can.
609 fn build_hosts(&self) -> &'static [&'static str] {
610 &[]
611 }
612
613 /// Report constructs this target cannot realize, with locations.
614 fn check(&self, app: &Application, config: &TargetConfig) -> Vec<Diagnostic>;
615
616 /// Generate sources and resources into the output directory.
617 fn generate(
618 &self,
619 app: &Application,
620 context: &BuildContext,
621 ) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>>;
622
623 /// Steps that turn generated files into the final product.
624 fn plan(&self, app: &Application, context: &BuildContext)
625 -> Result<BuildPlan, Vec<Diagnostic>>;
626
627 /// Steps that turn a release build, which `plan` has made, into the
628 /// packages people install, in [`dist_dir`].
629 fn package(
630 &self,
631 _app: &Application,
632 _context: &BuildContext,
633 _signing: &Signing,
634 ) -> Result<BuildPlan, Vec<Diagnostic>> {
635 Err(vec![Diagnostic::error(format!(
636 "target `{}` cannot make packages yet",
637 self.descriptor().id
638 ))])
639 }
640}
641
642/// How packages are signed, from the environment of `bern package`.
643/// Certificates stay with the person or the CI service that builds; none
644/// of this goes into `Bern.toml`.
645#[derive(Clone, Debug, Default, PartialEq, Eq)]
646pub struct Signing {
647 /// `BERN_MACOS_SIGNING_IDENTITY`: a signing identity in the keychain,
648 /// for distribution a "Developer ID Application" one.
649 pub macos_identity: Option<String>,
650 /// `BERN_MACOS_NOTARY_PROFILE`: a keychain profile made with
651 /// `xcrun notarytool store-credentials`.
652 pub macos_notary_profile: Option<String>,
653 /// `BERN_WINDOWS_CERTIFICATE`: the SHA-1 thumbprint of a code signing
654 /// certificate in the user's store, or a `.pfx` file.
655 pub windows_certificate: Option<String>,
656 /// `BERN_WINDOWS_CERTIFICATE_PASSWORD`: the password of the `.pfx`.
657 pub windows_certificate_password: Option<String>,
658 /// `BERN_ANDROID_KEYSTORE`: the keystore holding the upload or release
659 /// key. Gradle reads it and the variables below from the environment;
660 /// the passwords never go into a file.
661 pub android_keystore: Option<String>,
662 /// `BERN_ANDROID_KEYSTORE_PASSWORD`.
663 pub android_keystore_password: Option<String>,
664 /// `BERN_ANDROID_KEY_ALIAS`: the key in the keystore.
665 pub android_key_alias: Option<String>,
666}
667
668impl Signing {
669 pub fn from_env() -> Signing {
670 let var = |name: &str| {
671 std::env::var(name)
672 .ok()
673 .filter(|value| !value.trim().is_empty())
674 };
675 Signing {
676 macos_identity: var("BERN_MACOS_SIGNING_IDENTITY"),
677 macos_notary_profile: var("BERN_MACOS_NOTARY_PROFILE"),
678 windows_certificate: var("BERN_WINDOWS_CERTIFICATE"),
679 windows_certificate_password: var("BERN_WINDOWS_CERTIFICATE_PASSWORD"),
680 android_keystore: var("BERN_ANDROID_KEYSTORE"),
681 android_keystore_password: var("BERN_ANDROID_KEYSTORE_PASSWORD"),
682 android_key_alias: var("BERN_ANDROID_KEY_ALIAS"),
683 }
684 }
685}
686
687/// Explicit registry of target modules, assembled by the build host.
688#[derive(Default)]
689pub struct Registry {
690 targets: Vec<Box<dyn Target>>,
691}
692
693/// Default UI technology per platform.
694pub fn default_ui(platform: &str) -> Option<&'static str> {
695 match platform {
696 "macos" | "ios" | "tvos" => Some("swiftui"),
697 "linux" => Some("gtk"),
698 "windows" => Some("winui3"),
699 "android" => Some("compose"),
700 "browser" => Some("web"),
701 _ => None,
702 }
703}
704
705impl Registry {
706 pub fn new() -> Self {
707 Self::default()
708 }
709
710 pub fn register(&mut self, target: Box<dyn Target>) {
711 self.targets.push(target);
712 }
713
714 pub fn descriptors(&self) -> Vec<TargetDescriptor> {
715 self.targets
716 .iter()
717 .map(|target| target.descriptor())
718 .collect()
719 }
720
721 /// Resolve the target for a configuration. A missing default backend is
722 /// an error; there is never a silent switch to another UI technology.
723 pub fn resolve(&self, config: &TargetConfig) -> Result<&dyn Target, Diagnostic> {
724 let ui = match &config.ui {
725 Some(ui) => ui.clone(),
726 None => default_ui(&config.platform)
727 .ok_or_else(|| {
728 Diagnostic::error(format!(
729 "target `{}`: unknown platform `{}`",
730 config.name, config.platform
731 ))
732 })?
733 .to_string(),
734 };
735 self.targets
736 .iter()
737 .map(AsRef::as_ref)
738 .find(|target| {
739 let descriptor = target.descriptor();
740 descriptor.ui == ui && descriptor.platforms.contains(&config.platform.as_str())
741 })
742 .ok_or_else(|| {
743 Diagnostic::error(format!(
744 "target `{}`: the `{ui} + {}` backend is not available in this build of Bern",
745 config.name, config.platform
746 ))
747 })
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 struct Fake;
756
757 impl Target for Fake {
758 fn descriptor(&self) -> TargetDescriptor {
759 TargetDescriptor {
760 styles: &[],
761 motion: &[],
762 window_chrome: false,
763 text_marks: false,
764 dynamic_items: false,
765 features: &[],
766 id: "fake-macos",
767 ui: "swiftui",
768 platforms: &["macos"],
769 version: "0",
770 ir_versions: IR_VERSION..=IR_VERSION,
771 color_source: ColorSource::Native,
772 components: &[],
773 windows: WindowCapabilities {
774 native: true,
775 virtual_windows: false,
776 floating: true,
777 },
778 channels: &[],
779 media: None,
780 }
781 }
782 fn check(&self, _: &Application, _: &TargetConfig) -> Vec<Diagnostic> {
783 vec![]
784 }
785 fn generate(
786 &self,
787 _: &Application,
788 _: &BuildContext,
789 ) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>> {
790 Ok(vec![])
791 }
792 fn plan(&self, _: &Application, _: &BuildContext) -> Result<BuildPlan, Vec<Diagnostic>> {
793 Ok(BuildPlan::default())
794 }
795 }
796
797 fn config(platform: &str, ui: Option<&str>) -> TargetConfig {
798 TargetConfig {
799 name: "t".into(),
800 platform: platform.into(),
801 ui: ui.map(String::from),
802 web_mode: None,
803 web_extension: None,
804 webview: None,
805 javascript: false,
806 android: None,
807 channels: vec![],
808 frontends: vec![],
809 }
810 }
811
812 #[test]
813 fn defaults_resolve_by_platform_and_missing_backends_fail() {
814 let mut registry = Registry::new();
815 registry.register(Box::new(Fake));
816 assert_eq!(
817 registry
818 .resolve(&config("macos", None))
819 .unwrap()
820 .descriptor()
821 .id,
822 "fake-macos"
823 );
824 let error = registry.resolve(&config("linux", None)).err().unwrap();
825 assert!(error.message.contains("gtk + linux"), "{}", error.message);
826 assert!(registry.resolve(&config("macos", Some("flutter"))).is_err());
827 assert!(registry.resolve(&config("amiga", None)).is_err());
828 }
829
830 #[test]
831 fn generated_crates_name_bern_crates_like_the_project() {
832 let path = Framework::Path(PathBuf::from("/src/Bern/crates"));
833 assert_eq!(
834 path.dependency("bern-ffi"),
835 r#"path = "/src/Bern/crates/bern-ffi""#
836 );
837 let git = Framework::Git {
838 url: "https://github.com/AimesSoft/Bern".into(),
839 reference: Some(("branch".into(), "master".into())),
840 };
841 assert_eq!(
842 git.dependency("bern-ffi"),
843 r#"git = "https://github.com/AimesSoft/Bern", branch = "master""#
844 );
845 assert_eq!(
846 Framework::Registry("0.1".into()).dependency("bern-ffi"),
847 r#"version = "0.1""#
848 );
849 assert_eq!(toml_string(r#"C:\a "b""#), r#""C:\\a \"b\"""#);
850 }
851}
852
853pub mod script;