///|
pub(all) enum BundleArtifactKind {
AppBundleArtifact
DiskImageArtifact
ExecutableArtifact
InstallerArtifact
PortableDirectoryArtifact
DesktopEntryArtifact
} derive(Debug, Eq)
///|
pub(all) enum BundleRuntimeDependencyKind {
SystemWebViewRuntime
NativeLoaderLibrary
} derive(Debug, Eq)
///|
pub struct BundleArtifact {
kind : BundleArtifactKind
path : String
description : String
} derive(Debug, Eq)
///|
pub struct BundleRuntimeDependency {
kind : BundleRuntimeDependencyKind
name : String
path : String?
source_path : String?
required : Bool
description : String
} derive(Debug, Eq)
///|
pub(all) enum SigningPrerequisiteKind {
SigningIdentity
SigningTool
NotarizationTool
RuntimeDependency
PackageValidator
} derive(Debug, Eq)
///|
pub struct SigningPrerequisite {
kind : SigningPrerequisiteKind
name : String
description : String
} derive(Debug, Eq)
///|
pub struct SigningStep {
name : String
tool : String
arguments : Array[String]
description : String
} derive(Debug, Eq)
///|
pub struct SigningConfig {
identity : String?
team_id : String?
entitlements_path : String?
notarization_profile : String?
timestamp_url : String?
} derive(Debug, Eq)
///|
pub fn SigningConfig::new(
identity? : String,
team_id? : String,
entitlements_path? : String,
notarization_profile? : String,
timestamp_url? : String,
) -> SigningConfig {
{ identity, team_id, entitlements_path, notarization_profile, timestamp_url }
}
///|
pub fn SigningConfig::identity(self : SigningConfig) -> String? {
self.identity
}
///|
pub fn SigningConfig::team_id(self : SigningConfig) -> String? {
self.team_id
}
///|
pub fn SigningConfig::entitlements_path(self : SigningConfig) -> String? {
self.entitlements_path
}
///|
pub fn SigningConfig::notarization_profile(self : SigningConfig) -> String? {
self.notarization_profile
}
///|
pub fn SigningConfig::timestamp_url(self : SigningConfig) -> String? {
self.timestamp_url
}
///|
pub fn SigningConfig::validate(self : SigningConfig) -> Array[String] {
let problems : Array[String] = []
if self.identity is Some("") {
problems.push("signing identity must not be empty")
}
if self.team_id is Some("") {
problems.push("signing team id must not be empty")
}
if self.entitlements_path is Some("") {
problems.push("signing entitlements path must not be empty")
}
if self.notarization_profile is Some("") {
problems.push("signing notarization profile must not be empty")
}
if self.timestamp_url is Some("") {
problems.push("signing timestamp url must not be empty")
}
problems
}
///|
pub fn SigningConfig::configured(self : SigningConfig) -> Bool {
self.identity is Some(_) ||
self.team_id is Some(_) ||
self.entitlements_path is Some(_) ||
self.notarization_profile is Some(_) ||
self.timestamp_url is Some(_)
}
///|
pub fn SigningConfig::to_json(self : SigningConfig) -> String {
let fields : Array[String] = []
match self.identity {
Some(value) => fields.push("\"identity\":\{value.json_string()}")
None => ()
}
match self.team_id {
Some(value) => fields.push("\"teamId\":\{value.json_string()}")
None => ()
}
match self.entitlements_path {
Some(value) => fields.push("\"entitlements\":\{value.json_string()}")
None => ()
}
match self.notarization_profile {
Some(value) => fields.push("\"notarizationProfile\":\{value.json_string()}")
None => ()
}
match self.timestamp_url {
Some(value) => fields.push("\"timestampUrl\":\{value.json_string()}")
None => ()
}
"{\{fields.join(",")}}"
}
///|
pub fn BundleArtifact::new(
kind~ : BundleArtifactKind,
path~ : String,
description~ : String,
) -> BundleArtifact {
{ kind, path, description }
}
///|
pub fn BundleArtifact::kind(self : BundleArtifact) -> BundleArtifactKind {
self.kind
}
///|
pub fn BundleArtifact::path(self : BundleArtifact) -> String {
self.path
}
///|
pub fn BundleArtifact::description(self : BundleArtifact) -> String {
self.description
}
///|
pub fn BundleArtifactKind::name(self : BundleArtifactKind) -> String {
match self {
AppBundleArtifact => "app-bundle"
DiskImageArtifact => "disk-image"
ExecutableArtifact => "executable"
InstallerArtifact => "installer"
PortableDirectoryArtifact => "portable-directory"
DesktopEntryArtifact => "desktop-entry"
}
}
///|
pub fn BundleArtifact::to_json(self : BundleArtifact) -> String {
[
"{",
"\"kind\":\{self.kind.name().json_string()},",
"\"path\":\{self.path.json_string()},",
"\"description\":\{self.description.json_string()}",
"}",
].join("")
}
///|
pub fn BundleRuntimeDependency::new(
kind~ : BundleRuntimeDependencyKind,
name~ : String,
path? : String,
source_path? : String,
required? : Bool = true,
description~ : String,
) -> BundleRuntimeDependency {
{ kind, name, path, source_path, required, description }
}
///|
pub fn BundleRuntimeDependency::kind(
self : BundleRuntimeDependency,
) -> BundleRuntimeDependencyKind {
self.kind
}
///|
pub fn BundleRuntimeDependency::name(self : BundleRuntimeDependency) -> String {
self.name
}
///|
pub fn BundleRuntimeDependency::path(self : BundleRuntimeDependency) -> String? {
self.path
}
///|
pub fn BundleRuntimeDependency::source_path(
self : BundleRuntimeDependency,
) -> String? {
self.source_path
}
///|
pub fn BundleRuntimeDependency::required(
self : BundleRuntimeDependency,
) -> Bool {
self.required
}
///|
pub fn BundleRuntimeDependency::description(
self : BundleRuntimeDependency,
) -> String {
self.description
}
///|
pub fn BundleRuntimeDependencyKind::name(
self : BundleRuntimeDependencyKind,
) -> String {
match self {
SystemWebViewRuntime => "system-webview-runtime"
NativeLoaderLibrary => "native-loader-library"
}
}
///|
pub fn BundleRuntimeDependency::to_json(
self : BundleRuntimeDependency,
) -> String {
[
"{",
"\"kind\":\{self.kind.name().json_string()},",
"\"name\":\{self.name.json_string()},",
"\"required\":\{self.required.json_bool()},",
"\"path\":\{optional_bundle_json_string(self.path)},",
"\"sourcePath\":\{optional_bundle_json_string(self.source_path)},",
"\"description\":\{self.description.json_string()}",
"}",
].join("")
}
///|
pub fn SigningPrerequisite::new(
kind~ : SigningPrerequisiteKind,
name~ : String,
description~ : String,
) -> SigningPrerequisite {
{ kind, name, description }
}
///|
pub fn SigningPrerequisite::kind(
self : SigningPrerequisite,
) -> SigningPrerequisiteKind {
self.kind
}
///|
pub fn SigningPrerequisite::name(self : SigningPrerequisite) -> String {
self.name
}
///|
pub fn SigningPrerequisite::description(self : SigningPrerequisite) -> String {
self.description
}
///|
pub fn SigningPrerequisiteKind::name(self : SigningPrerequisiteKind) -> String {
match self {
SigningIdentity => "signing-identity"
SigningTool => "signing-tool"
NotarizationTool => "notarization-tool"
RuntimeDependency => "runtime-dependency"
PackageValidator => "package-validator"
}
}
///|
pub fn SigningPrerequisite::to_json(self : SigningPrerequisite) -> String {
[
"{",
"\"kind\":\{self.kind.name().json_string()},",
"\"name\":\{self.name.json_string()},",
"\"description\":\{self.description.json_string()}",
"}",
].join("")
}
///|
pub fn SigningStep::new(
name~ : String,
tool~ : String,
arguments~ : Array[String],
description~ : String,
) -> SigningStep {
{ name, tool, arguments, description }
}
///|
pub fn SigningStep::name(self : SigningStep) -> String {
self.name
}
///|
pub fn SigningStep::tool(self : SigningStep) -> String {
self.tool
}
///|
pub fn SigningStep::arguments(self : SigningStep) -> Array[String] {
self.arguments.copy()
}
///|
pub fn SigningStep::description(self : SigningStep) -> String {
self.description
}
///|
pub fn SigningStep::to_json(self : SigningStep) -> String {
[
"{",
"\"name\":\{self.name.json_string()},",
"\"tool\":\{self.tool.json_string()},",
"\"arguments\":[\{self.arguments.map(fn(argument) { argument.json_string() }).join(",")}],",
"\"description\":\{self.description.json_string()}",
"}",
].join("")
}
///|
pub fn BundlePlan::signing(self : BundlePlan) -> SigningConfig {
self.signing
}
///|
pub fn BundlePlan::signing_steps(self : BundlePlan) -> Array[SigningStep] {
match self.target {
MacOS => self.macos_signing_steps()
Windows => self.windows_signing_steps()
Linux => self.linux_signing_steps()
}
}
///|
pub fn BundlePlan::artifacts(self : BundlePlan) -> Array[BundleArtifact] {
match self.target {
MacOS =>
[
BundleArtifact::new(
kind=AppBundleArtifact,
path=self.bundle_name(),
description="macOS application bundle directory",
),
BundleArtifact::new(
kind=DiskImageArtifact,
path="\{self.executable_name}-\{self.metadata.version()}.dmg",
description="macOS distributable disk image",
),
]
Windows =>
[
BundleArtifact::new(
kind=ExecutableArtifact,
path="\{self.executable_name}/\{self.bundle_name()}",
description="Windows native launcher executable built from the generated launcher source",
),
BundleArtifact::new(
kind=InstallerArtifact,
path="\{self.executable_name}-\{self.metadata.version()}-setup.exe",
description="Windows installer package",
),
]
Linux =>
[
BundleArtifact::new(
kind=PortableDirectoryArtifact,
path=self.executable_name(),
description="Linux portable application directory",
),
BundleArtifact::new(
kind=DesktopEntryArtifact,
path="\{self.executable_name}/\{self.executable_name}.desktop",
description="Linux desktop integration entry",
),
]
}
}
///|
pub fn BundlePlan::runtime_dependencies(
self : BundlePlan,
) -> Array[BundleRuntimeDependency] {
match self.target {
MacOS =>
[
BundleRuntimeDependency::new(
kind=SystemWebViewRuntime,
name="WKWebView",
description="Provided by macOS; required by the native Lepusa runtime",
),
]
Windows =>
[
BundleRuntimeDependency::new(
kind=SystemWebViewRuntime,
name="Microsoft Edge WebView2 Runtime",
description="Required on target machines unless the installer provisions a fixed or evergreen WebView2 runtime",
),
BundleRuntimeDependency::new(
kind=NativeLoaderLibrary,
name="WebView2Loader.dll",
path="\{self.executable_name()}/WebView2Loader.dll",
source_path=self.runtime_dependency_source_path(
"WebView2Loader.dll",
default="_build/native/debug/build/cmd/runtime/WebView2Loader.dll",
),
description="Required beside the Windows Lepusa runtime so it can load the WebView2 API without a wrapper layer",
),
]
Linux =>
[
BundleRuntimeDependency::new(
kind=SystemWebViewRuntime,
name="WebKitGTK 4.1 or 4.0",
description="Required on target machines for the native Linux WebView backend",
),
]
}
}
///|
fn BundlePlan::runtime_dependency_source_path(
self : BundlePlan,
name : String,
default~ : String,
) -> String {
for source in self.runtime_dependency_sources {
if source.name() == name {
return source.source_path()
}
}
default
}
///|
pub fn BundlePlan::signing_prerequisites(
self : BundlePlan,
) -> Array[SigningPrerequisite] {
match self.target {
MacOS =>
[
SigningPrerequisite::new(
kind=SigningIdentity,
name="Developer ID Application certificate",
description="Required to sign distributable macOS apps outside the Mac App Store",
),
SigningPrerequisite::new(
kind=SigningTool,
name="codesign",
description="Required to apply the app signature and hardened runtime options",
),
SigningPrerequisite::new(
kind=NotarizationTool,
name="notarytool",
description="Required to submit signed macOS apps for Apple notarization",
),
]
Windows =>
[
SigningPrerequisite::new(
kind=SigningIdentity,
name="code signing certificate",
description="Required to sign Windows executables and installers",
),
SigningPrerequisite::new(
kind=SigningTool,
name="signtool",
description="Required to apply Authenticode signatures",
),
SigningPrerequisite::new(
kind=RuntimeDependency,
name="WebView2 Runtime",
description="Required on target machines unless bundled by the installer",
),
]
Linux => {
let prerequisites = [
SigningPrerequisite::new(
kind=SigningIdentity,
name="package signing key",
description="Required for signed deb, rpm, or repository distribution",
),
SigningPrerequisite::new(
kind=PackageValidator,
name="desktop-file-validate",
description="Required to validate Linux desktop integration metadata",
),
]
if self.signing.identity() is Some(_) {
prerequisites.push(
SigningPrerequisite::new(
kind=SigningTool,
name="gpg",
description="Required to create detached package signatures from configured signing identity",
),
)
}
prerequisites
}
}
}
///|
fn optional_bundle_json_string(value : String?) -> String {
match value {
Some(text) => text.json_string()
None => "null"
}
}
///|
fn BundlePlan::macos_signing_steps(self : BundlePlan) -> Array[SigningStep] {
let steps : Array[SigningStep] = []
match self.signing.identity() {
Some(identity) => {
let arguments = [
"--force", "--deep", "--options", "runtime", "--sign", identity,
]
match self.signing.entitlements_path() {
Some(path) => {
arguments.push("--entitlements")
arguments.push(path)
}
None => ()
}
arguments.push(self.bundle_name())
steps.push(
SigningStep::new(
name="codesign-app",
tool="codesign",
arguments~,
description="Apply a hardened-runtime signature to the macOS app bundle",
),
)
}
None => ()
}
match self.signing.notarization_profile() {
Some(profile) =>
steps.push(
SigningStep::new(
name="notarize-app",
tool="xcrun",
arguments=[
"notarytool",
"submit",
self.bundle_name(),
"--keychain-profile",
profile,
"--wait",
],
description="Submit the signed macOS app bundle for notarization",
),
)
None => ()
}
steps
}
///|
fn BundlePlan::windows_signing_steps(self : BundlePlan) -> Array[SigningStep] {
let steps : Array[SigningStep] = []
match self.signing.identity() {
Some(identity) => {
let arguments = ["sign", "/fd", "SHA256", "/n", identity]
match self.signing.timestamp_url() {
Some(url) => {
arguments.push("/tr")
arguments.push(url)
arguments.push("/td")
arguments.push("SHA256")
}
None => ()
}
arguments.push(self.bundle_name())
steps.push(
SigningStep::new(
name="sign-executable",
tool="signtool",
arguments~,
description="Apply an Authenticode signature to the Windows launcher artifact",
),
)
}
None => ()
}
steps
}
///|
fn BundlePlan::linux_signing_steps(self : BundlePlan) -> Array[SigningStep] {
let steps : Array[SigningStep] = []
match self.signing.identity() {
Some(identity) =>
steps.push(
SigningStep::new(
name="sign-package",
tool="gpg",
arguments=[
"--detach-sign",
"--local-user",
identity,
self.executable_name(),
],
description="Create a detached signature for the Linux package artifact",
),
)
None => ()
}
steps
}