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