///|
pub(all) enum BundleTarget {
MacOS
Windows
Linux
} derive(Debug, Eq)
///|
pub struct AppMetadata {
identifier : String
product_name : String
version : String
} derive(Debug, Eq)
///|
pub fn AppMetadata::new(
identifier~ : String,
product_name~ : String,
version~ : String,
) -> AppMetadata {
{ identifier, product_name, version }
}
///|
pub fn AppMetadata::validate(self : AppMetadata) -> Array[String] {
let problems : Array[String] = []
if self.identifier == "" {
problems.push("app identifier is required")
}
if self.product_name == "" {
problems.push("app product name is required")
}
if self.version == "" {
problems.push("app version is required")
}
problems
}
///|
pub fn AppMetadata::executable_name(self : AppMetadata) -> String {
self.product_name.replace_all(old=" ", new="-").to_lower()
}
///|
pub fn AppMetadata::identifier(self : AppMetadata) -> String {
self.identifier
}
///|
pub fn AppMetadata::product_name(self : AppMetadata) -> String {
self.product_name
}
///|
pub fn AppMetadata::version(self : AppMetadata) -> String {
self.version
}
///|
pub struct BundleConfig {
metadata : AppMetadata
target : BundleTarget
icon_path : String?
resource_sources : Array[BundleResourceSource]
runtime_dependency_sources : Array[BundleRuntimeDependencySource]
signing : SigningConfig
} derive(Debug, Eq)
///|
pub fn BundleConfig::new(
metadata : AppMetadata,
target? : BundleTarget = MacOS,
icon_path? : String,
resource_sources? : Array[BundleResourceSource] = [],
runtime_dependency_sources? : Array[BundleRuntimeDependencySource] = [],
signing? : SigningConfig = SigningConfig::new(),
) -> BundleConfig {
{
metadata,
target,
icon_path,
resource_sources,
runtime_dependency_sources,
signing,
}
}
///|
pub fn BundleConfig::validate(self : BundleConfig) -> Array[String] {
let problems = self.metadata.validate()
validate_bundle_resource_sources(self.resource_sources, problems)
validate_runtime_dependency_sources(self.runtime_dependency_sources, problems)
for problem in self.signing.validate() {
problems.push(problem)
}
problems
}
///|
pub struct BundleResourceSource {
source_path : String
path : String
executable : Bool
} derive(Debug, Eq)
///|
pub fn BundleResourceSource::new(
source_path~ : String,
path~ : String,
executable? : Bool = false,
) -> BundleResourceSource {
{ source_path, path, executable }
}
///|
pub fn BundleResourceSource::source_path(self : BundleResourceSource) -> String {
self.source_path
}
///|
pub fn BundleResourceSource::path(self : BundleResourceSource) -> String {
self.path
}
///|
pub fn BundleResourceSource::executable(self : BundleResourceSource) -> Bool {
self.executable
}
///|
fn validate_bundle_resource_sources(
sources : Array[BundleResourceSource],
problems : Array[String],
) -> Unit {
let paths : Array[String] = []
for source in sources {
if source.source_path().trim().is_empty() {
problems.push("bundle resource source path must not be empty")
}
let path = source.path().trim().to_owned()
if path.is_empty() {
problems.push("bundle resource path must not be empty")
} else if path.has_prefix("/") ||
path.has_prefix("\\") ||
path.split("/").contains("..") ||
path.split("\\").contains("..") {
problems.push("bundle resource path must stay inside the bundle: \{path}")
} else if paths.contains(path) {
problems.push("bundle resource path must be unique: \{path}")
} else {
paths.push(path)
}
}
}
///|
pub struct BundleRuntimeDependencySource {
name : String
source_path : String
} derive(Debug, Eq)
///|
pub fn BundleRuntimeDependencySource::new(
name~ : String,
source_path~ : String,
) -> BundleRuntimeDependencySource {
{ name, source_path }
}
///|
pub fn BundleRuntimeDependencySource::name(
self : BundleRuntimeDependencySource,
) -> String {
self.name
}
///|
pub fn BundleRuntimeDependencySource::source_path(
self : BundleRuntimeDependencySource,
) -> String {
self.source_path
}
///|
fn validate_runtime_dependency_sources(
sources : Array[BundleRuntimeDependencySource],
problems : Array[String],
) -> Unit {
let names : Array[String] = []
for source in sources {
if source.name() == "" {
problems.push("runtime dependency source name must not be empty")
} else if names.contains(source.name()) {
problems.push(
"runtime dependency source must be unique: \{source.name()}",
)
} else {
names.push(source.name())
}
if source.source_path() == "" {
problems.push("runtime dependency source path must not be empty")
}
}
}
///|
pub struct BundlePlan {
metadata : AppMetadata
target : BundleTarget
runtime : RuntimePlan
registered_routes : Array[String]
executable_name : String
bundle_name : String
icon_path : String?
resource_sources : Array[BundleResourceSource]
runtime_dependency_sources : Array[BundleRuntimeDependencySource]
signing : SigningConfig
} derive(Debug, Eq)
///|
pub(all) enum BundleResourceKind {
IconResource
AssetResource
ExecutableResource
} derive(Debug, Eq)
///|
pub struct BundleResource {
kind : BundleResourceKind
source_path : String
path : String
} derive(Debug, Eq)
///|
pub struct BundleFile {
path : String
content : String
executable : Bool
} derive(Debug, Eq)
///|
pub fn BundleFile::new(
path~ : String,
content~ : String,
executable? : Bool = false,
) -> BundleFile {
{ path, content, executable }
}
///|
pub fn BundleResource::new(
kind~ : BundleResourceKind,
source_path~ : String,
path~ : String,
) -> BundleResource {
{ kind, source_path, path }
}
///|
pub fn BundleResource::kind(self : BundleResource) -> BundleResourceKind {
self.kind
}
///|
pub fn BundleResource::source_path(self : BundleResource) -> String {
self.source_path
}
///|
pub fn BundleResource::path(self : BundleResource) -> String {
self.path
}
///|
pub fn BundleResourceKind::name(self : BundleResourceKind) -> String {
match self {
IconResource => "icon"
AssetResource => "asset"
ExecutableResource => "executable"
}
}
///|
pub fn BundleResource::to_json(self : BundleResource) -> String {
[
"{",
"\"kind\":\{self.kind.name().json_string()},",
"\"sourcePath\":\{self.source_path.json_string()},",
"\"path\":\{self.path.json_string()}",
"}",
].join("")
}
///|
pub fn BundleFile::path(self : BundleFile) -> String {
self.path
}
///|
pub fn BundleFile::content(self : BundleFile) -> String {
self.content
}
///|
pub fn BundleFile::executable(self : BundleFile) -> Bool {
self.executable
}
///|
pub fn BundlePlan::bundle_name(self : BundlePlan) -> String {
self.bundle_name
}
///|
pub fn BundleTarget::name(self : BundleTarget) -> String {
match self {
MacOS => "macos"
Windows => "windows"
Linux => "linux"
}
}
///|
pub fn BundlePlan::target(self : BundlePlan) -> BundleTarget {
self.target
}
///|
pub fn BundlePlan::registered_routes(self : BundlePlan) -> Array[String] {
self.registered_routes.copy()
}
///|
pub fn BundlePlan::identifier(self : BundlePlan) -> String {
self.metadata.identifier()
}
///|
pub fn BundlePlan::executable_name(self : BundlePlan) -> String {
self.executable_name
}
///|
pub fn BundlePlan::runtime_executable_name(self : BundlePlan) -> String {
match self.target {
Windows => "lepusa-runtime.exe"
_ => "lepusa-runtime"
}
}
///|
pub fn BundlePlan::runtime_executable_path(self : BundlePlan) -> String {
match self.target {
MacOS =>
"\{self.bundle_name()}/Contents/MacOS/\{self.runtime_executable_name()}"
Windows => "\{self.executable_name()}/\{self.runtime_executable_name()}"
Linux => "\{self.executable_name()}/\{self.runtime_executable_name()}"
}
}
///|
pub fn BundlePlan::icon_path(self : BundlePlan) -> String? {
self.icon_path
}
///|
pub fn BundlePlan::runtime_dependency_sources(
self : BundlePlan,
) -> Array[BundleRuntimeDependencySource] {
self.runtime_dependency_sources.copy()
}
///|
pub fn BundlePlan::resources(self : BundlePlan) -> Array[BundleResource] {
let resources = self.configured_resources()
for resource in self.asset_resources() {
resources.push(resource)
}
match self.icon_path {
Some(source_path) =>
resources.push(
BundleResource::new(
kind=IconResource,
source_path~,
path=self.icon_resource_path(),
),
)
None => ()
}
resources
}
///|
fn BundlePlan::configured_resources(self : BundlePlan) -> Array[BundleResource] {
self.resource_sources.map(source => {
let kind = if source.executable() {
ExecutableResource
} else {
AssetResource
}
BundleResource::new(
kind~,
source_path=source.source_path(),
path=if source.executable() {
self.executable_resource_path(source.path())
} else {
self.shared_path(source.path())
},
)
})
}
///|
fn BundlePlan::executable_resource_path(
self : BundlePlan,
path : String,
) -> String {
match self.target {
MacOS => "\{self.bundle_name}/Contents/MacOS/\{path}"
Windows | Linux => "\{self.executable_name}/\{path}"
}
}
///|
fn BundlePlan::asset_resources(self : BundlePlan) -> Array[BundleResource] {
let resources : Array[BundleResource] = []
for window in self.runtime.windows() {
for mapping in window.source().protocol_mappings() {
if mapping.root().has_prefix("package:") {
resources.push(
BundleResource::new(
kind=AssetResource,
source_path=mapping.root()[8:].to_owned(),
path=self.packaged_asset_resource_path(window.label()),
),
)
}
}
}
resources
}
///|
pub fn BundlePlan::files(
self : BundlePlan,
) -> Result[Array[BundleFile], Array[String]] {
match self.runtime_manifest_file_json() {
Ok(runtime_manifest_file) => {
let files : Array[BundleFile] = []
for file in self.platform_files() {
files.push(file)
}
files.push(
BundleFile::new(
path=self.shared_path("lepusa/runtime.json"),
content=runtime_manifest_file,
),
)
files.push(
BundleFile::new(
path=self.shared_path("lepusa/distribution.json"),
content=self.distribution_manifest_json(),
),
)
Ok(files)
}
Err(problems) => Err(problems)
}
}
///|
fn BundlePlan::shared_path(self : BundlePlan, path : String) -> String {
match self.target {
MacOS => "\{self.bundle_name}/Contents/Resources/\{path}"
Windows => "\{self.executable_name}/\{path}"
Linux => "\{self.executable_name}/\{path}"
}
}
///|
fn BundlePlan::icon_resource_path(self : BundlePlan) -> String {
match self.target {
MacOS => "\{self.bundle_name}/Contents/Resources/AppIcon.icns"
Windows => "\{self.executable_name}/app.ico"
Linux => "\{self.executable_name}/\{self.executable_name}.png"
}
}
///|
pub fn BundlePlan::runtime_manifest(
self : BundlePlan,
) -> Result[RuntimeLaunchManifest, Array[String]] {
match
self.runtime.launch_manifest(
registered_routes=self.registered_routes,
restrict_registered=true,
) {
Ok(manifest) => Ok(with_bundle_packaged_roots(manifest))
Err(problems) => Err(problems)
}
}
///|
pub fn BundlePlan::runtime_manifest_json(
self : BundlePlan,
) -> Result[String, Array[String]] {
match self.runtime_manifest() {
Ok(runtime_manifest) => Ok(runtime_manifest.to_json())
Err(problems) => Err(problems)
}
}
///|
pub fn BundlePlan::distribution_manifest_json(self : BundlePlan) -> String {
[
"{",
"\"identifier\":\{self.identifier().json_string()},",
"\"productName\":\{self.metadata.product_name().json_string()},",
"\"version\":\{self.metadata.version().json_string()},",
"\"target\":\{self.target.name().json_string()},",
"\"executable\":\{self.executable_name().json_string()},",
"\"bundle\":\{self.bundle_name().json_string()},",
"\"runtimeExecutable\":\{self.runtime_executable_path().json_string()},",
"\"artifacts\":[\{self.artifacts().map(fn(artifact) { artifact.to_json() }).join(",")}],",
"\"resources\":[\{self.resources().map(fn(resource) { resource.to_json() }).join(",")}],",
"\"runtimeDependencies\":[\{self.runtime_dependencies().map(fn(dependency) { dependency.to_json() }).join(",")}],",
"\"signing\":\{self.signing.to_json()},",
"\"signingPrerequisites\":[\{self.signing_prerequisites().map(fn(item) { item.to_json() }).join(",")}],",
"\"signingSteps\":[\{self.signing_steps().map(fn(step) { step.to_json() }).join(",")}]",
"}",
].join("")
}
///|
fn with_bundle_packaged_roots(
manifest : RuntimeLaunchManifest,
) -> RuntimeLaunchManifest {
let protocol_mappings : Array[RuntimeProtocolBinding] = []
for binding in manifest.protocol_mappings {
if binding.root.has_prefix("package:") {
protocol_mappings.push({
..binding,
root: "package:\{packaged_asset_runtime_root(binding.window_label)}",
})
} else {
protocol_mappings.push(binding)
}
}
{ ..manifest, protocol_mappings, }
}
///|
fn packaged_asset_runtime_root(window_label : String) -> String {
"lepusa/assets/\{window_label.to_asset_segment()}"
}
///|
fn BundlePlan::packaged_asset_resource_path(
self : BundlePlan,
window_label : String,
) -> String {
self.shared_path(packaged_asset_runtime_root(window_label))
}
///|
fn BundlePlan::runtime_manifest_file_json(
self : BundlePlan,
) -> Result[String, Array[String]] {
match self.runtime_manifest_json() {
Ok(runtime_manifest) =>
Ok(
[
"{",
"\"identifier\":\{self.identifier().json_string()},",
"\"productName\":\{self.metadata.product_name().json_string()},",
"\"version\":\{self.metadata.version().json_string()},",
"\"target\":\{self.target.name().json_string()},",
"\"artifacts\":[\{self.artifacts().map(fn(artifact) { artifact.to_json() }).join(",")}],",
"\"resources\":[\{self.resources().map(fn(resource) { resource.to_json() }).join(",")}],",
"\"runtimeDependencies\":[\{self.runtime_dependencies().map(fn(dependency) { dependency.to_json() }).join(",")}],",
"\"signing\":\{self.signing.to_json()},",
"\"signingPrerequisites\":[\{self.signing_prerequisites().map(fn(item) { item.to_json() }).join(",")}],",
"\"signingSteps\":[\{self.signing_steps().map(fn(step) { step.to_json() }).join(",")}],",
"\"runtime\":\{runtime_manifest}",
"}",
].join(""),
)
Err(problems) => Err(problems)
}
}
///|
pub fn BundleConfig::plan(
self : BundleConfig,
runtime : RuntimePlan,
registered_routes? : Array[String] = [],
) -> Result[BundlePlan, Array[String]] {
let problems = self.validate()
if problems.is_empty() {
let executable_name = self.metadata.executable_name()
let extension = match self.target {
MacOS => ".app"
Windows => ".exe"
Linux => ""
}
Ok({
metadata: self.metadata,
target: self.target,
runtime,
registered_routes: registered_routes.copy(),
executable_name,
bundle_name: executable_name + extension,
icon_path: self.icon_path,
resource_sources: self.resource_sources.copy(),
runtime_dependency_sources: self.runtime_dependency_sources.copy(),
signing: self.signing,
})
} else {
Err(problems)
}
}
///|