///|
async fn validate_bundle(layout : ProjectLayout, target : ServerTarget) -> Unit {
guard target is Native else {
fail("--bundle requires --server-target native.")
}
if layout.server_entry is Some(entry) {
let stub = source_path_to_string(entry.join("warren_assets.mbt"))
guard @fs.exists(stub) && @fs.kind(stub, follow_symlink=false) is Regular else {
fail(
"Server entry is not configured for bundling: missing warren_assets.mbt. For new projects use `warren new --template server`; for existing projects follow the server migration steps in Warren's README.",
)
}
}
if layout.public_dir is Some(dir) {
validate_bundle_tree(dir)
}
}
///|
// Reject links and special files before copying: following a directory link can
// otherwise recurse forever or accidentally embed files outside public/.
async fn validate_bundle_tree(dir : SourcePath) -> Unit {
for
name in @fs.readdir(
source_path_to_string(dir),
include_hidden=true,
sort=true,
) {
let path = dir.join(name)
match @fs.kind(source_path_to_string(path), follow_symlink=false) {
Regular => ()
Directory => validate_bundle_tree(path)
_ => fail("Bundle inputs must be regular files or directories: `\{path}`")
}
}
}
///|
fn bundle_bytes_expression(bytes : Bytes) -> String {
guard bytes.length() > 4096 else { bytes.to_string() }
// A multiline Bytes literal compiles to static storage, without rebuilding
// the resource on the heap. Keep source lines short for large binary files.
let out = StringBuilder()
out.write_string("[\n")
for i, byte in bytes {
if i % 4096 == 0 {
out.write_string(" ")
}
out.write_string("0x\{byte.to_hex()}, ")
if i % 4096 == 4095 {
out.write_string("\n")
}
}
out.write_string("\n ]")
out.to_string()
}
///|
async fn render_bundle_assets(dir : SourcePath) -> String {
let out = StringBuilder()
out.write_string(
"// Generated by warren build --bundle.\n///|\nfn warren_assets() -> Map[String, Bytes] {\n {\n",
)
async fn append_files(folder : SourcePath, prefix : String) -> Unit {
for
name in @fs.readdir(
source_path_to_string(folder),
include_hidden=true,
sort=true,
) {
let file = folder.join(name)
let key = "\{prefix}/\{name}"
match @fs.kind(source_path_to_string(file), follow_symlink=false) {
Directory => append_files(file, key)
Regular => {
let bytes = @fs.read_file(source_path_to_string(file)).binary()
out.write_string(
" \{key.escape()}: \{bundle_bytes_expression(bytes)},\n",
)
}
_ => fail("Cannot embed non-regular file: `\{file}`")
}
}
}
append_files(dir, "")
out.write_string(" }\n}\n")
out.to_string()
}
///|
async fn copy_bundle_entry(src : SourcePath, dst : SourcePath) -> Unit {
for
name in @fs.readdir(
source_path_to_string(src),
include_hidden=true,
sort=true,
) {
if name == "_build" ||
name == ".git" ||
name == ".mooncakes" ||
name == "dist" {
continue
}
let file = src.join(name)
match @fs.kind(source_path_to_string(file), follow_symlink=false) {
Directory => {
@fs.mkdir(source_path_to_string(dst.join(name)), permission=0o700)
copy_bundle_entry(file, dst.join(name))
}
Regular => {
copy(src=file, dst=dst.join(name))
if @fs.can_execute(source_path_to_string(file)) {
chmod_executable(dst.join(name))
}
}
_ =>
fail(
"Bundle server sources must not contain links or special files: `\{file}`",
)
}
}
}
///|
async fn build_bundle(
layout : ProjectLayout,
target : ServerTarget,
dist : SourcePath,
) -> Unit {
validate_bundle(layout, target)
let temp_string = @fs.tmpdir(prefix="warren-bundle")
defer @fs.rmdir(temp_string, recursive=true)
let temp = SourcePath::new(temp_string)
// Keep generated server artifacts under the same success/failure cleanup.
let target_dir = temp.join("build")
let browser_artifact = run_build_only(
layout.root,
layout.browser_entry,
"js",
true,
)
let static_dir = temp.join("static")
assemble_static(
layout.root,
layout.public_dir,
browser_artifact,
static_dir,
true,
)
let source = render_bundle_assets(static_dir)
if layout.server_entry is Some(entry) {
// A sibling copy preserves both module-relative build rules and relative
// paths to neighboring native stubs/libraries in the entry manifest.
let parent = SourcePath::new(
Path::dirname(source_path_to_string(entry)).to_string(),
)
let unique_name = Path::basename(temp_string)
.to_owned()
.replace_all(old=".", new="_")
.replace_all(old="-", new="_")
let staging = parent.join(unique_name)
@fs.mkdir(source_path_to_string(staging), permission=0o700)
defer @fs.rmdir(source_path_to_string(staging), recursive=true)
copy_bundle_entry(entry, staging)
@fs.write_file(
source_path_to_string(staging.join("warren_assets.mbt")),
source,
create_mode=CreateOrTruncate,
permission=0o600,
)
let artifact = run_build_only(
layout.root,
staging,
"native",
true,
target_dir~,
)
let artifact_name = Path::basename(source_path_to_string(artifact)).to_owned()
let entry_name = Path::basename(source_path_to_string(entry)).to_owned()
let output_name = if artifact_name.has_prefix(unique_name) {
"\{entry_name}\{artifact_name[unique_name.length():]}"
} else {
artifact_name
}
ensure_clean_dir(dist)
ignore(copy_server_artifact(artifact, dist, Native, output_name~))
} else {
let entry = temp.join("server.mbtx")
@fs.write_file(
source_path_to_string(entry),
bundle_server_source + "\n" + source,
create_mode=CreateOrTruncate,
permission=0o600,
)
let artifact = run_build_only(
layout.root,
entry,
"native",
true,
target_dir~,
)
ensure_clean_dir(dist)
ignore(copy_server_artifact(artifact, dist, Native))
}
log("build", "Bundled executable written to \{dist}")
}