// `moui package ` — produce distributable artifacts:
// Windows portable folders with `run.cmd`, macOS `.app` bundles, and web
// bundles. Ports the shell helpers (`package_windows_app_msvc.ps1`,
// `package-macos-app.sh`, `package-web-app.mjs`) into the CLI. Invoked
// without positional arguments, `moui package` keeps the legacy release
// planner behavior (`package_report.mbt`).
///|
priv struct ParsedPackageBuild {
help : Bool
json : Bool
platform : String
app : String
options : PackageBuildOptions
}
///|
priv struct PackageBuildOptions {
output_dir : String?
name : String?
version : String
build_number : String
bundle_id : String?
renderer : String
no_build : Bool
dry_run : Bool
}
///|
fn package_build_usage() -> String {
(
#|Usage: moui package [options]
#|
#|Produce a distributable artifact for a desktop or web target:
#|windows -> portable folder with run.cmd, macos -> .app bundle,
#|web -> static web bundle. Linux packaging is not implemented; use
#|`moui build linux` and run the raw executable.
#|
#|Arguments:
#| platform Target platform: macos, windows, or web.
#| app Project directory holding the entrypoint packages (or
#| the entrypoint package itself).
#|
#|Options:
#| --output DIR Distribution root. Defaults to dist/.
#| --name NAME App display name. Defaults from the project id.
#| --version VERSION Version recorded in the manifest. Defaults to 0.1.0.
#| --build-number N Build number recorded in the manifest. Defaults to 1.
#| --bundle-id ID macOS bundle identifier. Defaults to dev.wzzc.moui.app.
#| --renderer NAME Entrypoint override (auto / skia / wgpu / sun).
#| --no-build Package the existing build output without rebuilding.
#| --dry-run Print the planned layout and manifest without writing.
#| --json Emit machine-readable output.
#| -h, --help Show this help.
)
}
///|
/// Route to the real packager when the first positional argument names a
/// supported platform; otherwise the legacy release planner runs.
fn package_build_requested(args : Array[String]) -> Bool {
match args.get(0) {
Some(first) =>
!first.has_prefix("-") &&
(
desktop_platform_supported(first) ||
first == "android" ||
first == "ios" ||
first == "harmonyos"
)
None => false
}
}
///|
fn parse_package_build_args(
args : Array[String],
cwd : String,
) -> Result[ParsedPackageBuild, String] {
let _ = cwd
let help = Ref(false)
let json = Ref(false)
let output_dir : Ref[String?] = Ref(None)
let name : Ref[String?] = Ref(None)
let version : Ref[String?] = Ref(None)
let build_number : Ref[String?] = Ref(None)
let bundle_id : Ref[String?] = Ref(None)
let renderer : Ref[String?] = Ref(None)
let no_build = Ref(false)
let dry_run = Ref(false)
let positional : Array[String] = []
let mut index = 0
while index < args.length() {
let argument = args[index]
let (flag, inline) = option_parts(argument)
match flag {
"--json" =>
match flag_without_value(inline, flag) {
Ok(_) => json.val = true
Err(message) => return Err(message)
}
"-h" | "--help" =>
match flag_without_value(inline, flag) {
Ok(_) => help.val = true
Err(message) => return Err(message)
}
"--no-build" | "--dry-run" =>
match flag_without_value(inline, flag) {
Ok(_) =>
match flag {
"--no-build" => no_build.val = true
"--dry-run" => dry_run.val = true
_ => ()
}
Err(message) => return Err(message)
}
"--output"
| "--name"
| "--version"
| "--build-number"
| "--bundle-id"
| "--renderer" =>
match option_value(args, index, inline, flag) {
Ok((value, next)) => {
match flag {
"--output" => output_dir.val = Some(value)
"--name" => name.val = Some(value)
"--version" => version.val = Some(value)
"--build-number" => build_number.val = Some(value)
"--bundle-id" => bundle_id.val = Some(value)
"--renderer" => renderer.val = Some(value)
_ => ()
}
index = next
continue
}
Err(message) => return Err(message)
}
_ if argument.has_prefix("-") =>
return Err("unknown option for moui package: " + flag)
_ => positional.push(argument)
}
index += 1
}
if help.val {
return Ok({
help: true,
json: json.val,
platform: "",
app: "",
options: {
output_dir: None,
name: None,
version: "0.1.0",
build_number: "1",
bundle_id: None,
renderer: "auto",
no_build: false,
dry_run: false,
},
})
}
if positional.length() < 2 {
return Err("usage: moui package [options]")
}
if positional.length() > 2 {
return Err("moui package accepts exactly two positional arguments")
}
let platform = positional[0]
let app = positional[1]
if platform == "linux" {
return Err(
"Linux packaging is not implemented; use `moui build linux ` and run the raw executable, or `moui package linux --help` after support lands",
)
}
if platform == "android" || platform == "ios" || platform == "harmonyos" {
return Err(
"mobile artifacts are produced by `moui build " +
platform +
"` and verified with `moui verify`; moui package supports macos, windows, and web",
)
}
if !desktop_platform_supported(platform) {
return Err(
"unsupported platform for moui package: " +
platform +
" (expected macos, windows, or web)",
)
}
Ok({
help: false,
json: json.val,
platform,
app,
options: {
output_dir: output_dir.val,
name: name.val,
version: version.val.unwrap_or("0.1.0"),
build_number: build_number.val.unwrap_or("1"),
bundle_id: bundle_id.val,
renderer: renderer.val.unwrap_or("auto"),
no_build: no_build.val,
dry_run: dry_run.val,
},
})
}
///|
fn copy_file(source : String, destination : String) -> Result[Unit, String] {
let bytes = @fs.read_file_to_bytes(source) catch {
_ => return Err("could not read file for packaging: " + source)
}
@fs.write_bytes_to_file(destination, bytes) catch {
_ => return Err("could not write packaged file: " + destination)
}
Ok(())
}
///|
fn find_existing_file(candidates : Array[String]) -> String? {
for candidate in candidates {
if candidate != "" && path_is_file(candidate) {
return Some(candidate)
}
}
None
}
///|
fn locate_manifest_validator(build_base : String) -> String? {
find_existing_file([
join_path(build_base, "scripts/validate-package-manifest.mjs"),
"scripts/validate-package-manifest.mjs",
join_path(
moon_workspace_root(build_base).unwrap_or(""),
"scripts/validate-package-manifest.mjs",
),
])
}
///|
fn validate_package_manifest(
manifest_path : String,
platform : String,
build_base : String,
) -> String {
match locate_manifest_validator(build_base) {
Some(script) =>
match
process_exec_argv(
["node", script, manifest_path, "--platform", platform],
capture=true,
) {
Ok(result) =>
if result.exit_code == 0 {
"manifest validated"
} else {
"manifest validation failed: " + result.stderr
}
Err(message) => "manifest validation skipped: " + message
}
None => "manifest validation skipped (validator script not found)"
}
}
///|
fn package_build_outcome(
platform : String,
app : String,
output_path : String,
validator_note : String,
json : Bool,
) -> CliOutcome {
if json {
let fields : Map[String, Json] = Map([])
fields["schemaVersion"] = Json::number(1.0)
fields["ok"] = Json::boolean(true)
fields["command"] = Json::string("package")
fields["platform"] = Json::string(platform)
fields["app"] = Json::string(app)
fields["output"] = Json::string(output_path)
fields["validation"] = Json::string(validator_note)
stdout_outcome(Json::object(fields).stringify(indent=2) + "\n")
} else {
let mut text = "[package] " + platform + " app=" + app + "\n"
text += " output: " + output_path + "\n"
text += " " + validator_note + "\n"
stdout_outcome(text)
}
}
///|
fn run_package_build(args : Array[String], cwd : String) -> CliOutcome {
let json = json_requested(args)
let parsed = match parse_package_build_args(args, cwd) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, package_build_usage())
}
if parsed.help {
return stdout_outcome(package_build_usage())
}
let json = parsed.json
let options = parsed.options
let flow = match
resolve_desktop_flow(
cwd,
parsed.app,
parsed.platform,
renderer=options.renderer,
) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, package_build_usage())
}
let name = match options.name {
Some(value) => value
None => product_name(base_name(flow.app_root))
}
let dist_root = resolve_path(
cwd,
options.output_dir.unwrap_or("dist/" + parsed.platform),
)
let artifact = if options.no_build || options.dry_run {
desktop_artifact_path(
flow.build_base,
flow.package_path,
flow.target.target,
release=parsed.platform == "web",
)
} else {
match desktop_build_once(flow, detect_host_platform()) {
Ok(value) => value
Err(message) =>
return failed_outcome(message, json, package_build_usage())
}
}
match parsed.platform {
"windows" =>
package_windows_portable(flow, options, name, dist_root, artifact, json)
"macos" =>
package_macos_bundle(flow, options, name, dist_root, artifact, json)
"web" => package_web_bundle(flow, options, dist_root, json, cwd)
_ =>
failed_outcome(
"packaging for " + parsed.platform + " is not implemented",
json,
package_build_usage(),
)
}
}
///|
fn package_windows_portable(
flow : DesktopFlow,
options : PackageBuildOptions,
name : String,
dist_root : String,
artifact : String,
json : Bool,
) -> CliOutcome {
let app_dir = join_path(dist_root, name)
let executable = name + ".exe"
let manifest_path = join_path(app_dir, "moui-package.json")
if options.dry_run {
let mut text = "[package] windows app=" + name + " (dry-run)\n"
text += " output: " + app_dir + "\n"
text += " files: " +
executable +
", run.cmd, moui-package.json (+ zlib DLL)\n"
return stdout_outcome(text)
}
if !path_is_file(artifact) {
return failed_outcome(
"build artifact not found: " + artifact,
json,
package_build_usage(),
)
}
remove_tree(app_dir)
match mkdir_p(app_dir) {
Ok(_) => ()
Err(message) => return failed_outcome(message, json, package_build_usage())
}
match copy_file(artifact, join_path(app_dir, executable)) {
Ok(_) => ()
Err(message) => return failed_outcome(message, json, package_build_usage())
}
let runtime_files = ["run.cmd"]
match msvc_zlib_root(flow.app_root) {
Some(triplet) =>
match msvc_zlib_runtime_dll(triplet) {
Some(dll) => {
let dll_name = base_name(dll)
match copy_file(dll, join_path(app_dir, dll_name)) {
Ok(_) => runtime_files.push(dll_name)
Err(message) =>
return failed_outcome(message, json, package_build_usage())
}
}
None => ()
}
None => ()
}
let run_cmd = [
"@echo off",
"setlocal",
"set \"PATH=%~dp0;%PATH%\"",
"\"%~dp0" + executable + "\" %*",
"exit /b %ERRORLEVEL%",
].join("\r\n") +
"\r\n"
@fs.write_bytes_to_file(
join_path(app_dir, "run.cmd"),
@utf8.encode(run_cmd[:], bom=false),
) catch {
_ =>
return failed_outcome(
"could not write run.cmd",
json,
package_build_usage(),
)
}
let fields : Map[String, Json] = Map([])
fields["schemaVersion"] = Json::number(1.0)
fields["platform"] = Json::string("windows")
fields["outputKind"] = Json::string("portable-folder")
fields["appName"] = Json::string(name)
fields["moonPackage"] = Json::string(flow.package_path)
fields["version"] = Json::string(options.version)
fields["buildNumber"] = Json::string(options.build_number)
fields["executable"] = Json::string(executable)
fields["bundleName"] = Json::string(name)
fields["runtimeFiles"] = runtime_files.to_json()
let manifest = Json::object(fields).stringify(indent=2) + "\n"
@fs.write_bytes_to_file(manifest_path, @utf8.encode(manifest[:], bom=false)) catch {
_ =>
return failed_outcome(
"could not write moui-package.json",
json,
package_build_usage(),
)
}
let note = validate_package_manifest(
manifest_path,
"windows",
flow.build_base,
)
package_build_outcome("windows", name, app_dir, note, json)
}
///|
fn package_macos_bundle(
flow : DesktopFlow,
options : PackageBuildOptions,
name : String,
dist_root : String,
artifact : String,
json : Bool,
) -> CliOutcome {
let bundle_id = options.bundle_id.unwrap_or("dev.wzzc.moui.app")
let bundle_dir = join_path(dist_root, name + ".app")
let macos_dir = join_path(bundle_dir, "Contents/MacOS")
let resources_dir = join_path(bundle_dir, "Contents/Resources")
let executable = base_name(artifact)
let executable = if executable.has_suffix(".exe") {
executable[:executable.length() - 4].to_owned()
} else {
executable
}
let manifest_path = join_path(resources_dir, "moui-package.json")
if options.dry_run {
let mut text = "[package] macos app=" + name + " (dry-run)\n"
text += " output: " + bundle_dir + "\n"
text += " files: Contents/MacOS/" +
executable +
", Contents/Resources/moui-package.json\n"
return stdout_outcome(text)
}
if !path_is_file(artifact) {
return failed_outcome(
"build artifact not found: " + artifact,
json,
package_build_usage(),
)
}
remove_tree(bundle_dir)
match mkdir_p(macos_dir) {
Ok(_) => ()
Err(message) => return failed_outcome(message, json, package_build_usage())
}
match mkdir_p(resources_dir) {
Ok(_) => ()
Err(message) => return failed_outcome(message, json, package_build_usage())
}
let binary_path = join_path(macos_dir, executable)
match copy_file(artifact, binary_path) {
Ok(_) => ()
Err(message) => return failed_outcome(message, json, package_build_usage())
}
match make_executable(binary_path) {
Ok(_) => ()
Err(message) => return failed_outcome(message, json, package_build_usage())
}
let plist = "\n" +
"\n" +
"\n\n" +
" CFBundleExecutable\n " +
executable +
"\n" +
" CFBundleIdentifier\n " +
bundle_id +
"\n" +
" CFBundleName\n " +
name +
"\n" +
" CFBundlePackageType\n APPL\n" +
" CFBundleShortVersionString\n " +
options.version +
"\n" +
" CFBundleVersion\n " +
options.build_number +
"\n" +
" LSMinimumSystemVersion\n 12.0\n" +
" NSHighResolutionCapable\n \n" +
"\n\n"
@fs.write_bytes_to_file(
join_path(bundle_dir, "Contents/Info.plist"),
@utf8.encode(plist[:], bom=false),
) catch {
_ =>
return failed_outcome(
"could not write Info.plist",
json,
package_build_usage(),
)
}
let fields : Map[String, Json] = Map([])
fields["schemaVersion"] = Json::number(1.0)
fields["platform"] = Json::string("macos")
fields["outputKind"] = Json::string("app-bundle")
fields["appName"] = Json::string(name)
fields["moonPackage"] = Json::string(flow.package_path)
fields["bundleIdentifier"] = Json::string(bundle_id)
fields["version"] = Json::string(options.version)
fields["buildNumber"] = Json::string(options.build_number)
fields["executable"] = Json::string(executable)
fields["bundleName"] = Json::string(name + ".app")
fields["runtimeFiles"] = ([] : Array[String]).to_json()
let manifest = Json::object(fields).stringify(indent=2) + "\n"
@fs.write_bytes_to_file(manifest_path, @utf8.encode(manifest[:], bom=false)) catch {
_ =>
return failed_outcome(
"could not write moui-package.json",
json,
package_build_usage(),
)
}
let note = validate_package_manifest(manifest_path, "macos", flow.build_base)
package_build_outcome("macos", name, bundle_dir, note, json)
}
///|
/// The web bundle is produced by the shared Node packager; the CLI locates
/// it in the workspace (or the installed MoUI package) and streams its
/// output. The script performs its own release+strip build.
fn package_web_bundle(
flow : DesktopFlow,
options : PackageBuildOptions,
dist_root : String,
json : Bool,
cwd : String,
) -> CliOutcome {
if options.dry_run {
let mut text = "[package] web (dry-run)\n"
text += " output: " + dist_root + "\n"
text += " files: index.html, *.wasm, runtime.js, .gz/.br siblings, bundle-size.json\n"
return stdout_outcome(text)
}
let script = match
find_existing_file([
join_path(flow.build_base, "scripts/package-web-app.mjs"),
"scripts/package-web-app.mjs",
join_path(cwd, "scripts/package-web-app.mjs"),
join_path(
moon_workspace_root(cwd).unwrap_or(""),
".mooncakes/wzzc-dev/moui/scripts/package-web-app.mjs",
),
]) {
Some(path) => path
None =>
return failed_outcome(
"package-web-app.mjs was not found; serve the built wasm statically instead (`moui build web` + a static file server)",
json,
package_build_usage(),
)
}
let out_dir = join_path(dist_root, base_name(flow.app_root))
let argv = ["node", script, flow.package_path, "--out", out_dir]
if options.no_build {
argv.push("--no-build")
}
let result = match
process_exec_argv(argv, cwd=flow.build_base, capture=false) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, package_build_usage())
}
if result.exit_code != 0 {
return failed_outcome(
"web packaging failed with exit code " + result.exit_code.to_string(),
json,
package_build_usage(),
)
}
package_build_outcome(
"web",
flow.package_path,
out_dir,
"web bundle written",
json,
)
}