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 /// The Android ABIs a release is built for, one APK each; empty for
442 /// the 64-bit ARM alone, and for other platforms.
443 pub abis: Vec<String>,
444}
445
446/// The Android ABIs a release can be built for.
447pub const ANDROID_ABIS: &[&str] = &["arm64-v8a", "armeabi-v7a", "x86_64"];
448
449/// Android capabilities needed by an application's Rust engine, which
450/// cannot be inferred from its UI. Multicast also enables network access.
451#[derive(Clone, Debug, Default, serde::Deserialize)]
452#[serde(default, deny_unknown_fields)]
453pub struct AndroidOptions {
454 pub network: bool,
455 pub multicast: bool,
456 /// The engine changes how the system plays its audio.
457 pub audio_settings: bool,
458}
459
460impl TargetConfig {
461 pub fn has_webview(&self) -> bool {
462 self.platform == "browser"
463 || (self.platform != "tvos"
464 && self.webview.unwrap_or(matches!(
465 self.platform.as_str(),
466 "macos" | "ios" | "android" | "windows" | "linux" | "browser"
467 )))
468 }
469
470 pub fn javascript_backend(&self) -> Option<&'static str> {
471 self.javascript.then(|| {
472 if self.has_webview() {
473 if self.platform == "browser" {
474 "browser"
475 } else {
476 "webview"
477 }
478 } else {
479 "boa"
480 }
481 })
482 }
483}
484
485/// An application-owned browser adapter. Its `index.js` exports a setup function.
486#[derive(Clone, Debug, serde::Deserialize)]
487#[serde(deny_unknown_fields)]
488pub struct WebExtension {
489 pub path: String,
490 #[serde(default)]
491 pub build: Vec<String>,
492}
493
494/// The Windows front ends: WinUI 3 on the Windows App Runtime, and the
495/// system XAML through XAML Islands, which every Windows 10 and 11 has.
496pub const WINDOWS_FRONTENDS: [&str; 2] = ["winui3", "xaml-islands"];
497
498/// Where the Bern crates come from: the source of the project's own `bern`
499/// dependency. Generated crates take every Bern crate from it, so the
500/// application and the code generated around it share one copy of each.
501#[derive(Clone, Debug, PartialEq, Eq)]
502pub enum Framework {
503 /// A checkout whose `crates` directory holds every Bern crate.
504 Path(PathBuf),
505 /// A Git repository, at the reference the project names, if any:
506 /// `("branch", …)`, `("tag", …)` or `("rev", …)`.
507 Git {
508 url: String,
509 reference: Option<(String, String)>,
510 },
511 /// A release on crates.io, with the project's version requirement.
512 Registry(String),
513}
514
515impl Framework {
516 /// The dependency on the Bern crate `name`, as the inside of an inline
517 /// table, for example `path = "/src/bern/crates/bern-ffi"`.
518 pub fn dependency(&self, name: &str) -> String {
519 match self {
520 Framework::Path(crates) => {
521 let path = crates.join(name).display().to_string();
522 format!("path = {}", toml_string(&path.replace('\\', "/")))
523 }
524 Framework::Git { url, reference } => {
525 let mut out = format!("git = {}", toml_string(url));
526 if let Some((kind, value)) = reference {
527 out.push_str(&format!(", {kind} = {}", toml_string(value)));
528 }
529 out
530 }
531 Framework::Registry(version) => format!("version = {}", toml_string(version)),
532 }
533 }
534}
535
536/// A TOML basic string.
537pub fn toml_string(text: &str) -> String {
538 let mut out = String::from("\"");
539 for c in text.chars() {
540 match c {
541 '"' => out.push_str("\\\""),
542 '\\' => out.push_str("\\\\"),
543 c if c.is_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
544 c => out.push(c),
545 }
546 }
547 out.push('"');
548 out
549}
550
551/// Paths and packages a target needs to plan its build.
552#[derive(Clone, Debug)]
553pub struct BuildContext {
554 /// Directory containing `Bern.toml`.
555 pub project_dir: PathBuf,
556 /// Cargo workspace root.
557 pub workspace_dir: PathBuf,
558 /// `target/bern/<target-name>/`, owned by this target.
559 pub output_dir: PathBuf,
560 pub target: TargetConfig,
561 pub protocol_package: String,
562 pub protocol_path: PathBuf,
563 /// Rust path of the protocol module, for example `showcase_protocol::showcase`.
564 pub protocol_module: String,
565 pub engine_package: String,
566 pub engine_path: PathBuf,
567 /// Rust path of the engine type, which implements `Default`.
568 pub engine_type: String,
569 /// The application's modules, in catalog order.
570 pub modules: Vec<ModuleBuild>,
571 /// Where generated crates take the Bern crates from.
572 pub framework: Framework,
573 pub release: bool,
574}
575
576/// The crates of one application module, for the generated glue.
577#[derive(Clone, Debug)]
578pub struct ModuleBuild {
579 pub id: String,
580 pub protocol_package: String,
581 pub protocol_path: PathBuf,
582 /// Rust path of the module's protocol module, e.g. `downloads_protocol::downloads`.
583 pub protocol_module: String,
584 pub engine_package: String,
585 pub engine_path: PathBuf,
586 /// Rust path of the module engine type, which implements `Default`.
587 pub engine_type: String,
588 /// Whether the module can be switched off at run time.
589 pub optional: bool,
590 /// Whether an optional module runs before the application saved a choice.
591 pub enabled: bool,
592 pub depends: Vec<String>,
593}
594
595#[derive(Clone, Debug, PartialEq, Eq)]
596pub struct GeneratedFile {
597 /// Path relative to the output directory.
598 pub path: PathBuf,
599 pub contents: Vec<u8>,
600}
601
602impl GeneratedFile {
603 pub fn text(path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
604 Self {
605 path: path.into(),
606 contents: contents.into().into_bytes(),
607 }
608 }
609}
610
611/// A UI target module.
612pub trait Target: Send + Sync {
613 fn descriptor(&self) -> TargetDescriptor;
614
615 /// Operating systems that can build this target, as
616 /// `std::env::consts::OS` names them; empty when any can.
617 fn build_hosts(&self) -> &'static [&'static str] {
618 &[]
619 }
620
621 /// Report constructs this target cannot realize, with locations.
622 fn check(&self, app: &Application, config: &TargetConfig) -> Vec<Diagnostic>;
623
624 /// Generate sources and resources into the output directory.
625 fn generate(
626 &self,
627 app: &Application,
628 context: &BuildContext,
629 ) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>>;
630
631 /// Steps that turn generated files into the final product.
632 fn plan(&self, app: &Application, context: &BuildContext)
633 -> Result<BuildPlan, Vec<Diagnostic>>;
634
635 /// Steps that turn a release build, which `plan` has made, into the
636 /// packages people install, in [`dist_dir`].
637 fn package(
638 &self,
639 _app: &Application,
640 _context: &BuildContext,
641 _signing: &Signing,
642 ) -> Result<BuildPlan, Vec<Diagnostic>> {
643 Err(vec![Diagnostic::error(format!(
644 "target `{}` cannot make packages yet",
645 self.descriptor().id
646 ))])
647 }
648}
649
650/// How packages are signed, from the environment of `bern package`.
651/// Certificates stay with the person or the CI service that builds; none
652/// of this goes into `Bern.toml`.
653#[derive(Clone, Debug, Default, PartialEq, Eq)]
654pub struct Signing {
655 /// `BERN_MACOS_SIGNING_IDENTITY`: a signing identity in the keychain,
656 /// for distribution a "Developer ID Application" one.
657 pub macos_identity: Option<String>,
658 /// `BERN_MACOS_NOTARY_PROFILE`: a keychain profile made with
659 /// `xcrun notarytool store-credentials`.
660 pub macos_notary_profile: Option<String>,
661 /// `BERN_WINDOWS_CERTIFICATE`: the SHA-1 thumbprint of a code signing
662 /// certificate in the user's store, or a `.pfx` file.
663 pub windows_certificate: Option<String>,
664 /// `BERN_WINDOWS_CERTIFICATE_PASSWORD`: the password of the `.pfx`.
665 pub windows_certificate_password: Option<String>,
666 /// `BERN_ANDROID_KEYSTORE`: the keystore holding the upload or release
667 /// key. Gradle reads it and the variables below from the environment;
668 /// the passwords never go into a file.
669 pub android_keystore: Option<String>,
670 /// `BERN_ANDROID_KEYSTORE_PASSWORD`.
671 pub android_keystore_password: Option<String>,
672 /// `BERN_ANDROID_KEY_ALIAS`: the key in the keystore.
673 pub android_key_alias: Option<String>,
674}
675
676impl Signing {
677 pub fn from_env() -> Signing {
678 let var = |name: &str| {
679 std::env::var(name)
680 .ok()
681 .filter(|value| !value.trim().is_empty())
682 };
683 Signing {
684 macos_identity: var("BERN_MACOS_SIGNING_IDENTITY"),
685 macos_notary_profile: var("BERN_MACOS_NOTARY_PROFILE"),
686 windows_certificate: var("BERN_WINDOWS_CERTIFICATE"),
687 windows_certificate_password: var("BERN_WINDOWS_CERTIFICATE_PASSWORD"),
688 android_keystore: var("BERN_ANDROID_KEYSTORE"),
689 android_keystore_password: var("BERN_ANDROID_KEYSTORE_PASSWORD"),
690 android_key_alias: var("BERN_ANDROID_KEY_ALIAS"),
691 }
692 }
693}
694
695/// Explicit registry of target modules, assembled by the build host.
696#[derive(Default)]
697pub struct Registry {
698 targets: Vec<Box<dyn Target>>,
699}
700
701/// Default UI technology per platform.
702pub fn default_ui(platform: &str) -> Option<&'static str> {
703 match platform {
704 "macos" | "ios" | "tvos" => Some("swiftui"),
705 "linux" => Some("gtk"),
706 "windows" => Some("winui3"),
707 "android" => Some("compose"),
708 "browser" => Some("web"),
709 _ => None,
710 }
711}
712
713impl Registry {
714 pub fn new() -> Self {
715 Self::default()
716 }
717
718 pub fn register(&mut self, target: Box<dyn Target>) {
719 self.targets.push(target);
720 }
721
722 pub fn descriptors(&self) -> Vec<TargetDescriptor> {
723 self.targets
724 .iter()
725 .map(|target| target.descriptor())
726 .collect()
727 }
728
729 /// Resolve the target for a configuration. A missing default backend is
730 /// an error; there is never a silent switch to another UI technology.
731 pub fn resolve(&self, config: &TargetConfig) -> Result<&dyn Target, Diagnostic> {
732 let ui = match &config.ui {
733 Some(ui) => ui.clone(),
734 None => default_ui(&config.platform)
735 .ok_or_else(|| {
736 Diagnostic::error(format!(
737 "target `{}`: unknown platform `{}`",
738 config.name, config.platform
739 ))
740 })?
741 .to_string(),
742 };
743 self.targets
744 .iter()
745 .map(AsRef::as_ref)
746 .find(|target| {
747 let descriptor = target.descriptor();
748 descriptor.ui == ui && descriptor.platforms.contains(&config.platform.as_str())
749 })
750 .ok_or_else(|| {
751 Diagnostic::error(format!(
752 "target `{}`: the `{ui} + {}` backend is not available in this build of Bern",
753 config.name, config.platform
754 ))
755 })
756 }
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 struct Fake;
764
765 impl Target for Fake {
766 fn descriptor(&self) -> TargetDescriptor {
767 TargetDescriptor {
768 styles: &[],
769 motion: &[],
770 window_chrome: false,
771 text_marks: false,
772 dynamic_items: false,
773 features: &[],
774 id: "fake-macos",
775 ui: "swiftui",
776 platforms: &["macos"],
777 version: "0",
778 ir_versions: IR_VERSION..=IR_VERSION,
779 color_source: ColorSource::Native,
780 components: &[],
781 windows: WindowCapabilities {
782 native: true,
783 virtual_windows: false,
784 floating: true,
785 },
786 channels: &[],
787 media: None,
788 }
789 }
790 fn check(&self, _: &Application, _: &TargetConfig) -> Vec<Diagnostic> {
791 vec![]
792 }
793 fn generate(
794 &self,
795 _: &Application,
796 _: &BuildContext,
797 ) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>> {
798 Ok(vec![])
799 }
800 fn plan(&self, _: &Application, _: &BuildContext) -> Result<BuildPlan, Vec<Diagnostic>> {
801 Ok(BuildPlan::default())
802 }
803 }
804
805 fn config(platform: &str, ui: Option<&str>) -> TargetConfig {
806 TargetConfig {
807 name: "t".into(),
808 platform: platform.into(),
809 ui: ui.map(String::from),
810 web_mode: None,
811 web_extension: None,
812 webview: None,
813 javascript: false,
814 android: None,
815 abis: Vec::new(),
816 channels: vec![],
817 frontends: vec![],
818 }
819 }
820
821 #[test]
822 fn defaults_resolve_by_platform_and_missing_backends_fail() {
823 let mut registry = Registry::new();
824 registry.register(Box::new(Fake));
825 assert_eq!(
826 registry
827 .resolve(&config("macos", None))
828 .unwrap()
829 .descriptor()
830 .id,
831 "fake-macos"
832 );
833 let error = registry.resolve(&config("linux", None)).err().unwrap();
834 assert!(error.message.contains("gtk + linux"), "{}", error.message);
835 assert!(registry.resolve(&config("macos", Some("flutter"))).is_err());
836 assert!(registry.resolve(&config("amiga", None)).is_err());
837 }
838
839 #[test]
840 fn generated_crates_name_bern_crates_like_the_project() {
841 let path = Framework::Path(PathBuf::from("/src/Bern/crates"));
842 assert_eq!(
843 path.dependency("bern-ffi"),
844 r#"path = "/src/Bern/crates/bern-ffi""#
845 );
846 let git = Framework::Git {
847 url: "https://github.com/AimesSoft/Bern".into(),
848 reference: Some(("branch".into(), "master".into())),
849 };
850 assert_eq!(
851 git.dependency("bern-ffi"),
852 r#"git = "https://github.com/AimesSoft/Bern", branch = "master""#
853 );
854 assert_eq!(
855 Framework::Registry("0.1".into()).dependency("bern-ffi"),
856 r#"version = "0.1""#
857 );
858 assert_eq!(toml_string(r#"C:\a "b""#), r#""C:\\a \"b\"""#);
859 }
860}
861
862pub mod script;