pub mod brand_assets;
pub mod glue;
pub mod icon;
pub mod icon_set;
pub mod names;
pub mod skill;
pub mod style;
pub mod svg;
pub mod texts;
mod check;
mod plan;
mod report;
pub use check::check_common;
pub use plan::{
BuildPlan, BuildStep, Run, SimulatedSystem, archive, dist_dir, files_below,
package_architecture,
};
pub use report::{FeatureReport, feature_report};
use bern_ir::{Application, IR_VERSION, SourceLocation};
use std::fmt;
use std::ops::RangeInclusive;
use std::path::PathBuf;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorSource {
Native,
BernTheme,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WindowCapabilities {
pub native: bool,
pub virtual_windows: bool,
pub floating: bool,
}
#[derive(Clone, Debug)]
pub struct MediaSupport {
pub contract_versions: RangeInclusive<u32>,
pub backend: &'static str,
pub backend_version: &'static str,
pub presenter: &'static str,
pub presenter_version: &'static str,
pub background_playback: bool,
pub multi_view: bool,
}
#[derive(Clone, Debug)]
pub struct TargetDescriptor {
pub id: &'static str,
pub ui: &'static str,
pub platforms: &'static [&'static str],
pub version: &'static str,
pub ir_versions: RangeInclusive<u32>,
pub color_source: ColorSource,
pub components: &'static [&'static str],
pub windows: WindowCapabilities,
pub channels: &'static [&'static str],
pub media: Option<MediaSupport>,
pub styles: &'static [StyleSupport],
pub motion: &'static [&'static str],
pub window_chrome: bool,
pub text_marks: bool,
pub dynamic_items: bool,
pub features: &'static [&'static str],
}
pub mod feature {
pub const BUTTON_CONTENT: &str = "button_content";
pub const REVEAL: &str = "reveal";
pub const TOOLBAR_SEARCH: &str = "toolbar_search";
pub const SEARCH_RESULTS: &str = "search_results";
pub const SESSION_ACTIONS: &str = "session_actions";
pub const TOGGLE_BUTTON: &str = "toggle_button";
pub const DIALOG_WINDOWS: &str = "dialog_windows";
pub const BOUND_LENGTHS: &str = "bound_lengths";
pub const SCROLL_ANCHOR: &str = "scroll_anchor";
pub const SCROLL_BROWSING: &str = "scroll_browsing";
pub const BOUND_BLUR: &str = "bound_blur";
pub const TEXT_BOLD: &str = "text_bold";
pub const APPEARANCE: &str = "appearance";
pub const SHORTCUTS: &str = "shortcuts";
pub const BOUND_SHORTCUTS: &str = "bound_shortcuts";
pub const SHORTCUT_INPUT: &str = "shortcut_input";
pub const MENU_BAR: &str = "menu_bar";
pub const STATUS_ITEM: &str = "status_item";
pub const RUNTIME_THEME: &str = "runtime_theme";
pub const RUNTIME_PALETTE: &str = "runtime_palette";
pub const RUNTIME_IMAGES: &str = "runtime_images";
pub const PICTURE_FILLS: &str = "picture_fills";
pub const COVER_IMAGES: &str = "cover_images";
pub const FILE_PICK: &str = "file_pick";
pub const TABLE: &str = "table";
pub const CONTROLLED_TEXT: &str = "controlled_text";
pub const WATCH: &str = "watch";
pub const LOGIN_SWITCH: &str = "login_switch";
pub const PASTE: &str = "paste";
pub const SCROLL_SNAP: &str = "scroll_snap";
pub const RAILS: &str = "rails";
pub const POPOVER_BUTTON: &str = "popover_button";
pub const TEXT_WEIGHTS: &str = "text_weights";
pub const TEXT_LINES: &str = "text_lines";
pub const IMAGE_CORNERS: &str = "image_corners";
pub const SIDEBAR_WIDTH: &str = "sidebar_width";
pub const CONTROLLED_VALUES: &str = "controlled_values";
pub const SEARCH_PALETTE: &str = "search_palette";
pub const IMAGE_TEMPLATE: &str = "image_template";
pub const ADAPTIVE_IMAGES: &str = "adaptive_images";
pub const SCALED_SCENE: &str = "scaled_scene";
pub const ACCORDION_LAYOUT: &str = "accordion_layout";
pub const CAPSULE_CONTROLS: &str = "capsule_controls";
pub const MODAL_FLOW_TIMER: &str = "modal_flow_timer";
pub const PAGE_BADGES: &str = "page_badges";
pub const IDLE_CONTROL: &str = "idle_control";
pub const FILL_BUTTONS: &str = "fill_buttons";
pub const GRID_CELLS: &str = "grid_cells";
pub const PAGE_SEARCH: &str = "page_search";
pub const FORM_LAYOUT: &str = "form_layout";
pub const MODAL_COVER: &str = "modal_cover";
pub const WINDOW_LANDSCAPE: &str = "window_landscape";
pub const WINDOW_SHORT: &str = "window_short";
pub const TAB_ACCESSORY: &str = "tab_accessory";
pub const EDGE_GROW: &str = "edge_grow";
pub const DELAYED_APPEAR: &str = "delayed_appear";
pub const TOOLBAR_LEADING: &str = "toolbar_leading";
pub const AXIS_PADDING: &str = "axis_padding";
pub const ICON_MENUS: &str = "icon_menus";
pub const ICON_LABELS: &str = "icon_labels";
pub const ICON_TABS: &str = "icon_tabs";
pub const ROW_BASELINE: &str = "row_baseline";
pub const SIDEBAR_HEADER: &str = "sidebar_header";
pub const UNTITLED_DETAIL: &str = "untitled_detail";
pub const SIZE_CLASSES: &str = "size_classes";
pub const TEXT_FORMATS: &str = "text_formats";
}
pub fn uses_text_formats(app: &Application) -> bool {
use bern_protocol::format::{Piece, parse};
let formatted = app.texts.iter().any(|text| {
text.translations.values().any(|translation| {
parse(translation).is_ok_and(|pieces| {
pieces
.iter()
.any(|piece| !matches!(piece, Piece::Text(_) | Piece::Value(_)))
})
})
});
let labels_show_fields = app.types.enums.iter().any(|def| {
def.variants.iter().any(|variant| {
variant
.label
.as_ref()
.is_some_and(|label| !label.params.is_empty())
})
});
formatted || labels_show_fields
}
#[derive(Clone, Copy, Debug)]
pub struct StyleSupport {
pub component: &'static str,
pub base: &'static [&'static str],
pub states: &'static [(bern_ir::style::StyleState, &'static [&'static str])],
}
impl StyleSupport {
fn shows(&self, state: Option<bern_ir::style::StyleState>, effect: &str) -> bool {
match state {
None => self.base.contains(&effect),
Some(state) => self
.states
.iter()
.any(|(supported, effects)| *supported == state && effects.contains(&effect)),
}
}
}
impl TargetDescriptor {
pub fn supports_ir(&self) -> bool {
self.ir_versions.contains(&IR_VERSION)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Note,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub severity: Severity,
pub code: Option<&'static str>,
pub message: String,
pub location: Option<SourceLocation>,
}
impl Diagnostic {
pub fn error(message: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
code: None,
message: message.into(),
location: None,
}
}
pub fn warning(message: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
..Self::error(message)
}
}
pub fn note(message: impl Into<String>) -> Self {
Self {
severity: Severity::Note,
..Self::error(message)
}
}
pub fn code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
pub fn at(mut self, location: SourceLocation) -> Self {
self.location = Some(location);
self
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let level = match self.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Note => "note",
};
match self.code {
Some(code) => write!(f, "{level}[{code}]: {}", self.message)?,
None => write!(f, "{level}: {}", self.message)?,
}
if let Some(location) = &self.location {
write!(
f,
"\n --> {}:{}:{}",
location.file, location.line, location.column
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct TargetConfig {
pub name: String,
pub platform: String,
pub ui: Option<String>,
pub web_mode: Option<String>,
pub web_extension: Option<WebExtension>,
pub channels: Vec<String>,
pub frontends: Vec<String>,
pub webview: Option<bool>,
pub javascript: bool,
pub android: Option<AndroidOptions>,
}
#[derive(Clone, Debug, Default, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AndroidOptions {
pub network: bool,
pub multicast: bool,
}
impl TargetConfig {
pub fn has_webview(&self) -> bool {
self.platform == "browser"
|| (self.platform != "tvos"
&& self.webview.unwrap_or(matches!(
self.platform.as_str(),
"macos" | "ios" | "android" | "windows" | "linux" | "browser"
)))
}
pub fn javascript_backend(&self) -> Option<&'static str> {
self.javascript.then(|| {
if self.has_webview() {
if self.platform == "browser" {
"browser"
} else {
"webview"
}
} else {
"boa"
}
})
}
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WebExtension {
pub path: String,
#[serde(default)]
pub build: Vec<String>,
}
pub const WINDOWS_FRONTENDS: [&str; 2] = ["winui3", "xaml-islands"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Framework {
Path(PathBuf),
Git {
url: String,
reference: Option<(String, String)>,
},
Registry(String),
}
impl Framework {
pub fn dependency(&self, name: &str) -> String {
match self {
Framework::Path(crates) => {
let path = crates.join(name).display().to_string();
format!("path = {}", toml_string(&path.replace('\\', "/")))
}
Framework::Git { url, reference } => {
let mut out = format!("git = {}", toml_string(url));
if let Some((kind, value)) = reference {
out.push_str(&format!(", {kind} = {}", toml_string(value)));
}
out
}
Framework::Registry(version) => format!("version = {}", toml_string(version)),
}
}
}
pub fn toml_string(text: &str) -> String {
let mut out = String::from("\"");
for c in text.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
c if c.is_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Clone, Debug)]
pub struct BuildContext {
pub project_dir: PathBuf,
pub workspace_dir: PathBuf,
pub output_dir: PathBuf,
pub target: TargetConfig,
pub protocol_package: String,
pub protocol_path: PathBuf,
pub protocol_module: String,
pub engine_package: String,
pub engine_path: PathBuf,
pub engine_type: String,
pub modules: Vec<ModuleBuild>,
pub framework: Framework,
pub release: bool,
}
#[derive(Clone, Debug)]
pub struct ModuleBuild {
pub id: String,
pub protocol_package: String,
pub protocol_path: PathBuf,
pub protocol_module: String,
pub engine_package: String,
pub engine_path: PathBuf,
pub engine_type: String,
pub optional: bool,
pub enabled: bool,
pub depends: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GeneratedFile {
pub path: PathBuf,
pub contents: Vec<u8>,
}
impl GeneratedFile {
pub fn text(path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
Self {
path: path.into(),
contents: contents.into().into_bytes(),
}
}
}
pub trait Target: Send + Sync {
fn descriptor(&self) -> TargetDescriptor;
fn build_hosts(&self) -> &'static [&'static str] {
&[]
}
fn check(&self, app: &Application, config: &TargetConfig) -> Vec<Diagnostic>;
fn generate(
&self,
app: &Application,
context: &BuildContext,
) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>>;
fn plan(&self, app: &Application, context: &BuildContext)
-> Result<BuildPlan, Vec<Diagnostic>>;
fn package(
&self,
_app: &Application,
_context: &BuildContext,
_signing: &Signing,
) -> Result<BuildPlan, Vec<Diagnostic>> {
Err(vec![Diagnostic::error(format!(
"target `{}` cannot make packages yet",
self.descriptor().id
))])
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Signing {
pub macos_identity: Option<String>,
pub macos_notary_profile: Option<String>,
pub windows_certificate: Option<String>,
pub windows_certificate_password: Option<String>,
pub android_keystore: Option<String>,
pub android_keystore_password: Option<String>,
pub android_key_alias: Option<String>,
}
impl Signing {
pub fn from_env() -> Signing {
let var = |name: &str| {
std::env::var(name)
.ok()
.filter(|value| !value.trim().is_empty())
};
Signing {
macos_identity: var("BERN_MACOS_SIGNING_IDENTITY"),
macos_notary_profile: var("BERN_MACOS_NOTARY_PROFILE"),
windows_certificate: var("BERN_WINDOWS_CERTIFICATE"),
windows_certificate_password: var("BERN_WINDOWS_CERTIFICATE_PASSWORD"),
android_keystore: var("BERN_ANDROID_KEYSTORE"),
android_keystore_password: var("BERN_ANDROID_KEYSTORE_PASSWORD"),
android_key_alias: var("BERN_ANDROID_KEY_ALIAS"),
}
}
}
#[derive(Default)]
pub struct Registry {
targets: Vec<Box<dyn Target>>,
}
pub fn default_ui(platform: &str) -> Option<&'static str> {
match platform {
"macos" | "ios" | "tvos" => Some("swiftui"),
"linux" => Some("gtk"),
"windows" => Some("winui3"),
"android" => Some("compose"),
"browser" => Some("web"),
_ => None,
}
}
impl Registry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, target: Box<dyn Target>) {
self.targets.push(target);
}
pub fn descriptors(&self) -> Vec<TargetDescriptor> {
self.targets
.iter()
.map(|target| target.descriptor())
.collect()
}
pub fn resolve(&self, config: &TargetConfig) -> Result<&dyn Target, Diagnostic> {
let ui = match &config.ui {
Some(ui) => ui.clone(),
None => default_ui(&config.platform)
.ok_or_else(|| {
Diagnostic::error(format!(
"target `{}`: unknown platform `{}`",
config.name, config.platform
))
})?
.to_string(),
};
self.targets
.iter()
.map(AsRef::as_ref)
.find(|target| {
let descriptor = target.descriptor();
descriptor.ui == ui && descriptor.platforms.contains(&config.platform.as_str())
})
.ok_or_else(|| {
Diagnostic::error(format!(
"target `{}`: the `{ui} + {}` backend is not available in this build of Bern",
config.name, config.platform
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Fake;
impl Target for Fake {
fn descriptor(&self) -> TargetDescriptor {
TargetDescriptor {
styles: &[],
motion: &[],
window_chrome: false,
text_marks: false,
dynamic_items: false,
features: &[],
id: "fake-macos",
ui: "swiftui",
platforms: &["macos"],
version: "0",
ir_versions: IR_VERSION..=IR_VERSION,
color_source: ColorSource::Native,
components: &[],
windows: WindowCapabilities {
native: true,
virtual_windows: false,
floating: true,
},
channels: &[],
media: None,
}
}
fn check(&self, _: &Application, _: &TargetConfig) -> Vec<Diagnostic> {
vec![]
}
fn generate(
&self,
_: &Application,
_: &BuildContext,
) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>> {
Ok(vec![])
}
fn plan(&self, _: &Application, _: &BuildContext) -> Result<BuildPlan, Vec<Diagnostic>> {
Ok(BuildPlan::default())
}
}
fn config(platform: &str, ui: Option<&str>) -> TargetConfig {
TargetConfig {
name: "t".into(),
platform: platform.into(),
ui: ui.map(String::from),
web_mode: None,
web_extension: None,
webview: None,
javascript: false,
android: None,
channels: vec![],
frontends: vec![],
}
}
#[test]
fn defaults_resolve_by_platform_and_missing_backends_fail() {
let mut registry = Registry::new();
registry.register(Box::new(Fake));
assert_eq!(
registry
.resolve(&config("macos", None))
.unwrap()
.descriptor()
.id,
"fake-macos"
);
let error = registry.resolve(&config("linux", None)).err().unwrap();
assert!(error.message.contains("gtk + linux"), "{}", error.message);
assert!(registry.resolve(&config("macos", Some("flutter"))).is_err());
assert!(registry.resolve(&config("amiga", None)).is_err());
}
#[test]
fn generated_crates_name_bern_crates_like_the_project() {
let path = Framework::Path(PathBuf::from("/src/Bern/crates"));
assert_eq!(
path.dependency("bern-ffi"),
r#"path = "/src/Bern/crates/bern-ffi""#
);
let git = Framework::Git {
url: "https://github.com/AimesSoft/Bern".into(),
reference: Some(("branch".into(), "master".into())),
};
assert_eq!(
git.dependency("bern-ffi"),
r#"git = "https://github.com/AimesSoft/Bern", branch = "master""#
);
assert_eq!(
Framework::Registry("0.1".into()).dependency("bern-ffi"),
r#"version = "0.1""#
);
assert_eq!(toml_string(r#"C:\a "b""#), r#""C:\\a \"b\"""#);
}
}
pub mod script;