///|
/// The top-level `key = "value"` lines of a `moon.mod` (the text format).
fn parse_moon_mod(text : String) -> Map[String, String] {
let fields : Map[String, String] = Map([])
for raw in text.split("\n") {
let line = raw.trim().to_owned()
if line.has_prefix("//") ||
line.has_prefix("import") ||
line.has_prefix("}") {
continue
}
match line.find(" = \"") {
Some(at) if line.has_suffix("\"") => {
let key = line[0:at].to_owned()
let value = line[at + 4:line.length() - 1].to_owned()
if !fields.contains(key) && !key.contains(" ") && !key.contains("\"") {
fields[key] = value.replace_all(old="\\\"", new="\"")
}
}
_ => ()
}
}
fields
}
///|
fn json_string(map : Map[String, Json], key : String) -> String? {
match map.get(key) {
Some(String(text)) => Some(text)
_ => None
}
}
///|
/// The fields of the module description in `dir`: `moon.mod`, or the older
/// `moon.mod.json`.
fn read_moon_module(dir : String) -> Map[String, String] raise CliError {
let text_path = join_path(dir, "moon.mod")
if @fs.path_exists(text_path) {
return parse_moon_mod(@utf8.decode_lossy(read_bytes(text_path)[:]))
}
let json_path = join_path(dir, "moon.mod.json")
if @fs.path_exists(json_path) {
let parsed = @json.parse(@utf8.decode_lossy(read_bytes(json_path)[:])) catch {
err => raise Failed("\{json_path}: \{err}")
}
let fields : Map[String, String] = Map([])
if parsed is Object(map) {
for
key in [
"name", "version", "license", "repository", "description", "readme",
] {
if json_string(map, key) is Some(value) {
fields[key] = value
}
}
}
return fields
}
raise Failed("no moon.mod or moon.mod.json in \"\{dir}\"")
}
///|
fn is_executable_package(dir : String) -> Bool {
let text_path = join_path(dir, "moon.pkg")
if @fs.path_exists(text_path) {
let text = @utf8.decode_lossy(read_bytes(text_path)[:]) catch { _ => "" }
return text.contains("\"executable\"")
}
let json_path = join_path(dir, "moon.pkg.json")
if @fs.path_exists(json_path) {
let text = @utf8.decode_lossy(read_bytes(json_path)[:]) catch { _ => "" }
return text.contains("\"is-main\": true") ||
text.contains("\"is-main\":true")
}
false
}
///|
fn skipped_directory(name : String) -> Bool {
name.has_prefix(".") ||
name == "_build" ||
name == "target" ||
name == "node_modules" ||
name == "examples" ||
name == "docs"
}
///|
/// Relative paths of the executable packages under `dir` (the root is `""`).
fn find_executables(
root : String,
relative : String,
depth : Int32,
found : Array[String],
) -> Unit raise CliError {
let here = if relative == "" { root } else { join_path(root, relative) }
if is_executable_package(here) {
found.push(relative)
}
if depth >= 4 {
return
}
let listing = @fs.read_dir(here) catch { _ => [] }
let names = listing.to_owned()
names.sort()
for name in names {
if skipped_directory(name) {
continue
}
let child = if relative == "" { name } else { relative + "/" + name }
let is_dir = @fs.is_dir(join_path(root, child)) catch { _ => false }
if is_dir {
find_executables(root, child, depth + 1, found)
}
}
}
///|
/// A Debian-style package name from a module name: the part after the
/// slash, in lower case, with underscores turned into dashes.
fn package_name_of(module_name : String) -> String {
let last = match module_name.rev_find("/") {
Some(at) => module_name[at + 1:].to_owned()
None => module_name
}
last.to_lower().replace_all(old="_", new="-")
}
///|
fn json_text(text : String) -> String {
Json::string(text).stringify()
}
///|
/// `linuxpkg init --moon [DIR]`: write `DIR/linuxpkg.json` for the MoonBit
/// project in `DIR`, packaging the native executable it builds
/// (`moon build --target native --release`).
fn init_moon(options : Options) -> Int32 raise CliError {
let dir = match options.positional {
[] => "."
[path] => path
_ => raise Usage("init --moon takes at most one directory")
}
let fields = read_moon_module(dir)
let module_name = match fields.get("name") {
Some(name) => name
None => raise Failed("\"\{dir}\" has no module name in its moon.mod")
}
let found : Array[String] = []
find_executables(dir, "", 0, found)
if found.length() == 0 {
raise Failed(
"no executable package found in \"\{dir}\" (a moon.pkg with pkgtype(kind: \"executable\"))",
)
}
let chosen = match options.value("bin") {
Some(wanted) =>
if found.contains(wanted) {
wanted
} else {
raise Failed(
"no executable package \"\{wanted}\" in \"\{dir}\"; found: \{found.join(", ")}",
)
}
None =>
if found.length() == 1 {
found[0]
} else {
raise Failed(
"several executable packages (\{found.join(", ")}); choose one with --bin",
)
}
}
let package_dir = match chosen.rev_find("/") {
Some(at) => chosen[at + 1:].to_owned()
None => chosen
}
let base = package_name_of(module_name)
let name = match options.value("name") {
Some(name) => name
None => base
}
// The built file is named after the package directory; the installed name
// is the project's, unless the package has a name of its own.
let file_stem = if chosen == "" {
match module_name.rev_find("/") {
Some(at) => module_name[at + 1:].to_owned()
None => module_name
}
} else {
package_dir
}
let built = if chosen == "" {
"_build/native/release/build/\{file_stem}.exe"
} else {
"_build/native/release/build/\{chosen}/\{file_stem}.exe"
}
let command = if chosen == "" ||
package_dir == "main" ||
options.value("name") is Some(_) {
name
} else {
package_dir
}
// The architecture of a binary that is already built wins over the default:
// an arm64 build must not be packaged as amd64.
let built_arch : String? = if @fs.path_exists(join_path(dir, built)) {
try @elf.parse(read_bytes(join_path(dir, built))[:]) catch {
_ => None
} noraise {
info => info.arch()
}
} else {
None
}
let arch = match (options.value("arch"), built_arch) {
(Some(arch), Some(found)) => {
let same = try @spec.Arch::parse(arch) == @spec.Arch::parse(found) catch {
_ => true
} noraise {
equal => equal
}
if !same {
eprintln(
"linuxpkg: warning: \"\{built}\" is built for \{found}, but --arch says \{arch}; `build` will refuse this unless allow_arch_mismatch is set",
)
}
arch
}
(Some(arch), None) => arch
(None, Some(found)) => found
(None, None) => "amd64"
}
let version = fields.get("version").unwrap_or("0.1.0")
let description = fields.get("description").unwrap_or(name)
let contents : Array[String] = [
" { \"src\": \{json_text(built)}, \"dst\": \{json_text("/usr/bin/" + command)}, \"mode\": \"0755\" }",
]
if @fs.path_exists(join_path(dir, "README.md")) {
contents.push(
" { \"src\": \"README.md\", \"dst\": \{json_text("/usr/share/doc/" + name + "/README.md")} }",
)
}
if @fs.path_exists(join_path(dir, "LICENSE")) {
contents.push(
" { \"src\": \"LICENSE\", \"dst\": \{json_text("/usr/share/doc/" + name + "/copyright")} }",
)
}
let out = StringBuilder()
out.write_string("{\n")
out.write_string(" \"name\": \{json_text(name)},\n")
out.write_string(" \"version\": \{json_text(version)},\n")
out.write_string(" \"release\": \"1\",\n")
out.write_string(" \"arch\": \{json_text(arch)},\n")
out.write_string(" \"maintainer\": \"Your Name \",\n")
out.write_string(" \"description\": \{json_text(description)},\n")
if fields.get("repository") is Some(url) {
out.write_string(" \"homepage\": \{json_text(url)},\n")
}
if fields.get("license") is Some(license) {
out.write_string(" \"license\": \{json_text(license)},\n")
}
out.write_string(" \"contents\": [\n")
out.write_string(contents.join(",\n"))
out.write_string("\n ]\n}\n")
let target = join_path(dir, "linuxpkg.json")
if @fs.path_exists(target) && !options.flag("force") {
raise Failed("\"\{target}\" already exists, use --force to overwrite it")
}
write_bytes(target, @utf8.encode(out.to_string()))
println(
"wrote \{target} for the executable package \"\{if chosen == "" { "." } else { chosen }}\"",
)
println("Set \"maintainer\" in it, then:")
println(" moon build --target native --release")
println(" linuxpkg build \{target} --out dist")
0
}