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