LunaPack

    Application distribution packer built with MoonBit

    packaging
    distribution
    cli
    Download zip
    Author
    Version
    0.1.4
    License
    Apache-2.0
    Last updated
    44 minutes ago
    Downloads
    15

    No dependencies

    #LunaPack

    LunaPack is an application-distribution packer written in MoonBit. It bundles an application executable together with its resources into a single distributable file, and unpacks it again on the target machine with integrity checking.

    Repository: github.com/riantr/LunaPack (mirrored to gitee.com/ren-yongxiang/lunaPack for everyday storage).

    • SHA-256 checksums for every entry (verified on unpack).
    • MD5 fingerprints for every entry: the files are scanned before they go into the package, the fingerprints are listed, and after unpacking every extracted file is read back and checked against the fingerprint it had. The fingerprint of the produced file is reported as well.
    • gzip and LZ4 compression per entry. LZ4 is implemented in this package and trades a larger result for a much shorter packing time.
    • encryption of the entries, either to the owner of a public key or with a passphrase. The mechanism is the one of the reference implementation this package follows.
    • Cross-platform: runs on Windows, Linux and macOS; packages carry their install scripts with the platform they target.
    • Native and Wasm: usable as a standalone binary or through moonx.

    #Releases

    Prebuilt binaries are attached to every GitHub release:

    AssetPlatform
    lunapack-windows-x86_64.exeWindows
    lunapack-linux-x86_64Linux
    lunapack-macos-aarch64macOS (Apple Silicon)
    lunapack.wasmWebAssembly, for moonx
    lunapack.corethe compiled package the WebAssembly module needs

    The release is built by a GitHub workflow when a version tag such as v0.1.4 is pushed; the same version is published to the Mooncakes registry, so moonx riantr/LunaPack/cmd/main@latest resolves to it.

    The registry requires a 0.x.y version while it is in its early stage, so the releases are numbered 0.1.0, 0.1.1, 0.1.2, 0.1.3, 0.1.4 and so on.

    #Usage

    #Via moonx (published on Mooncakes)

    moonx riantr/LunaPack/cmd/main@latest \ --input-exe=[input-exe-path] \ --output-exe=[output-exe-path] \ --Files=[Pack-Folder]

    The coordinate names the package that holds the entry point, because the registry stores the prebuilt WebAssembly module per package. The entry point lives in cmd/main, so the coordinate is riantr/LunaPack/cmd/main@latest.

    FlagMeaning
    --input-exethe application executable to distribute
    --output-exethe produced single-file bundle
    --Filesfolder of files to bundle with it (repeatable)

    The result is a self-extracting bundle: the LunaPack launcher with the archive appended, exactly the trick a WinRAR or 7-Zip self-extracting archive uses.

    The bundle is self-extracting only when it is produced by a native LunaPack binary, because the launcher stub is the binary itself. A bundle produced through moonx runs on WebAssembly and therefore carries the payload without a native launcher; use the native binary to get a self-extracting file.

    myapp.exe = [ LunaPack launcher ] + [ archive: myapp.exe, res/app.cfg, ... ]

    Starting it extracts the payload and launches the bundled program:

    $ ./myapp.exe LunaPack: starting D:/apps/.lunapack/9c445884d23653fb/myapp.exe

    The payload is cached in .lunapack/<hash>/ next to the executable, so the second start is immediate. Delete that folder to force a fresh extraction.

    Because the launcher does the extraction, the application finds its own dependencies (.dll, .json, assets) exactly like it would in its build folder - a plain copy of the executable would not run at all.

    #The icon

    When the application carries an icon, the bundle gets it as well, so Explorer shows the application's own icon instead of the launcher's:

    $ lunapack --input-exe=./BRLabeler.exe --output-exe=./BRLabeler.exe --Files=./build md5 cb77181515d4680a960433f65ba05bef BRLabeler.exe ... scanned 392 files bundled 392 files -> ./BRLabeler.exe (icon copied) output md5 f5f06ca638d59a31e773c0a28358804d BRLabeler.exe

    The icon is read from the application executable with a small PE reader and written into the bundle with the Windows resource API, which creates the resource section the launcher does not have. All icon sizes are copied, so the bundle looks right at every zoom level.

    This is a Windows feature: an icon is a Windows resource, so on Linux and macOS the bundle keeps the icon of the launcher. Use --Files with the application's own .ico file there, or install the icon separately.

    lunapack unpack recognises the appended archive automatically, so the same file can be verified and extracted again:

    lunapack verify myapp.exe lunapack info myapp.exe lunapack unpack -i myapp.exe -o ./out

    #As a native binary

    moon build --release # -> _build/native/release/build/cmd/main/main.exe (every platform)

    The toolchain calls the executable main.exe on every platform, Windows included, so the path is the same everywhere.

    The package has no dependencies: everything it needs comes with the MoonBit toolchain, so there is nothing to download and moon update is not needed.

    lunapack pack -i <input_dir> -o <output.appkg> [options] lunapack unpack -i <package> -o <dest_dir> [--skip-scripts] lunapack verify <package> [--extracted <dir>] lunapack info <package>

    Flags accept both --flag=value and --flag value.

    #Options

    OptionMeaning
    --name <name>package name (default: input dir / exe name)
    --version <ver>package version (default: 0.1.0)
    --os <os>target OS tag (default: detected)
    --arch <arch>target arch tag (default: detected)
    --pre-install <file>script executed before extraction
    --post-install <file>script executed after extraction
    --compression-method <m>none, gzip, lz4 or auto (default: auto)
    --encryption-method <m>x25519 or passphrase (default: none)
    --public-key <file>the public key of the recipient
    --secret-key <file>the secret key of the recipient
    --passphrase-file <file>the file that holds the passphrase
    --skip-scriptsunpack without running the install scripts
    --extracted <dir>with verify: also check the files already in <dir>

    #Compression

    --compression-method picks the codec of the entries:

    MethodWhat it does
    nonestore the files as they are
    gzipgzip every entry
    lz4LZ4 every entry
    autogzip every entry, but keep the raw bytes when gzip would make the entry bigger (the default)

    LZ4 does no entropy coding at all, so it is much faster than gzip while producing a larger result. That is the trade to make when a package is built often and unpacked rarely. Measured on an application of 392 files and 332 MB:

    gzipLZ4
    packing39 s16 s
    unpacking21 s19 s
    size of the package139 MB175 MB

    The LZ4 implementation lives in this package, because the MoonBit toolchain ships none. What it produces is a standard .lz4 frame, so the lz4 tool decompresses it, and this package decompresses what that tool produces. Both directions are covered by the test suite.

    #Encryption

    --encryption-method seals the entries of a package. There are two methods:

    MethodWhat it does
    x25519the package is sealed to the owner of a public key; only the owner of the secret key can open it
    passphrasethe package is sealed with a passphrase

    The mechanism is the one of the reference implementation this package follows. A key is established with the recipient, HKDF-SHA512 turns it into the key of the package, and every entry is sealed on its own with ChaCha20-Poly1305 under a key and a nonce that are derived for it. The additional data of an entry holds its place in the package and its name, so that neither can be changed without the seal breaking. A check in the manifest tells a wrong key from a damaged package.

    The key exchange is the one part that is different. The reference uses the sntrup761 KEM, which the toolchain has no counterpart of, so this package uses X25519 - the curve that the reference itself carries as a part of its own sntrup761x25519-sha512 method.

    # Make a pair of keys. The public key goes to the machine that seals the # packages, the secret key to the machine that opens them. lunapack keygen --public-key recipient.pub --secret-key recipient.key # Seal a package to the owner of the public key. lunapack pack -i ./app -o app.appkg \ --encryption-method x25519 --public-key recipient.pub # Open it again with the secret key. lunapack unpack -i app.appkg -o ./out --secret-key recipient.key # Or with a passphrase. lunapack pack -i ./app -o app.appkg \ --encryption-method passphrase --passphrase-file passphrase.txt lunapack unpack -i app.appkg -o ./out --passphrase-file passphrase.txt

    A secret key that a passphrase protects is the better form: the raw form leaves the secret key in clear text on disk, and anyone who reads it can open every package that was ever sealed for it.

    lunapack keygen --public-key recipient.pub --secret-key recipient.key \ --passphrase-file passphrase.txt

    The self-extracting bundle finds its key next to itself: the file <bundle>.key for a public key, the file <bundle>.pass for a passphrase. The environment variables LUNAPACK_SECRET_KEY and LUNAPACK_PASSPHRASE_FILE name the files instead.

    Every primitive is implemented in this package, because the toolchain ships none of them: SHA-512, HMAC, HKDF, PBKDF2, ChaCha20, Poly1305 and X25519. Each one is checked against the vector that its standard publishes, and the derivation is checked against the reference implementation itself.

    #Example

    # Bundle an application and its resources into one self-extracting file. lunapack --input-exe=./build/myapp \ --output-exe=./dist/myapp \ --Files=./build # Or produce a plain archive instead. lunapack pack -i ./myapp -o myapp.appkg \ --name myapp --version 1.4.0 \ --pre-install setup.sh --post-install setup.sh lunapack verify myapp.appkg lunapack unpack -i myapp.appkg -o /opt/myapp

    Packing prints the MD5 fingerprint of every file it scans, and the fingerprint of the file it produces:

    md5 6f5902ac237024bdd0c176cb93063dc4 a.txt md5 3db2050fcf84bb631dcae417d3db518c sub/b.txt scanned 2 files packed 2 files -> myapp.appkg output md5 63da650c906fe3ab1446b121a4edd4ca myapp.appkg

    The fingerprint of the produced file changes from run to run, because the manifest records the time of creation.

    Unpacking reads every extracted file back and checks its fingerprint against the one that went into the package, so a file that changed on the way to the disk is reported:

    unpacked 2 files -> /opt/myapp md5 6f5902ac237024bdd0c176cb93063dc4 a.txt md5 3db2050fcf84bb631dcae417d3db518c sub/b.txt verified 2 files, 0 mismatches

    MD5 is not collision resistant, so it is only the fingerprint that can be compared with what the platform tooling shows; the integrity of the package rests on the SHA-256 checksum of every entry.

    Files that are already on the disk can be audited against the package, which is what the self-extracting bundle uses its cache for:

    lunapack verify BRLabeler.exe --extracted .lunapack\60844df795cb7f31

    On a cache that is intact:

    verify: OK (392 files) verify: OK (392 files in .lunapack/60844df795cb7f31) VERIFY OK

    After a file was changed on the disk:

    verify: OK (392 files) verify: md5 mismatch for appsettings.json: expected 86ff00ef... got 3ddcb685... verify: FAILED (392 files checked) VERIFY FAILED

    The bundle checks every file it writes as it extracts it, so the cache is verified once when it is filled. The check above is for auditing a cache that has been on the disk for a while.

    On Windows, with the paths of a real .NET application:

    lunapack ^ --input-exe="D:/src/App/bin/Release/net10.0-windows/App.exe" ^ --output-exe="D:/dist/App.exe" ^ --Files="D:/src/App/bin/Release/net10.0-windows"

    The contents of a single --Files directory are stored at the root of the archive next to the main program, so the extracted application is directly runnable. With several --Files directories each one gets a subdirectory named after it. The main program is never stored twice, even when it also lives inside the --Files folder.

    The binary to use is the one from the release, not moonx:

    D://src//WorkBuddy//Moonbit//LunaPack//_build//native//release//build//cmd//main//main.exe ^ --input-exe="D://zkhongtuo//src//MOlabeler_V2.6//bin//x64//Release//net10.0-windows10.0.22000.0//BRLabeler.exe" ^ --output-exe="D://zkhongtuo//src//MOLabeler_V2.6//bin//BRLabeler.exe" ^ --Files="D://zkhongtuo//src//MOlabeler_V2.6//bin//x64//Release//net10.0-windows10.0.22000.0"

    #Publishing to Mooncakes

    moonx resolves a package from the registry, so publish the module first:

    moon login # once moon publish --dry-run # validate moon publish # publish riantr/LunaPack moonx riantr/LunaPack/cmd/main@latest --input-exe=... --output-exe=... --Files=...

    The registry refuses a version that already exists, so a release needs a new number in moon.mod.

    The module publishes with supported_targets = "native+wasm"; moonx runs the Wasm build, which reads and writes the real filesystem through the MoonBit runtime.

    Because the package has no dependencies, the published module carries none either: nothing has to be resolved when it is fetched.

    #Install scripts

    The script body is stored verbatim inside the manifest together with the platform it targets. The platform is inferred from the file extension:

    ExtensionTarget platform
    .cmd, .bat, .ps1windows
    .shposix (Linux + macOS)
    anything elseany

    At unpack time the script matching the host platform runs; a script for a different platform is skipped, so one package can safely be unpacked anywhere. On Windows the script runs through cmd /c, on Linux/macOS through sh.

    The script runs with the destination directory as its working directory and receives two environment variables:

    • LUNAPACK_DEST - absolute path of the destination directory
    • LUNAPACK_PHASE - pre_install or post_install

    Use --skip-scripts to unpack without running them.

    #Package format

    The container layout has not changed since v2: the marker names the layout, and the manifest carries its own format_version. The current manifest is version 5, which adds the encryption of the entries. A package written by an older version still unpacks, because the fingerprint and the encryption are both optional.

    +--------------------+ offset 0 | magic "APPKGD02" | 8 bytes +--------------------+ offset 8 | entry payloads | stored back to back, raw, gzip, lz4 or sealed +--------------------+ | manifest (UTF-8 | | JSON) | +--------------------+ | magic "APPKGD02" | 8 bytes | manifest offset | 8 bytes, little-endian int64 | manifest length | 8 bytes, little-endian int64 +--------------------+ end of file

    The manifest records the package metadata plus one record per entry:

    { "format_version": 4, "name": "myapp", "version": "1.4.0", "os": "linux", "arch": "x86_64", "checksum_algo": "sha256", "compression": "auto", "created_at": "2026-11-04T04:10:20Z", "main_program": "myapp.exe", "pre_install": { "platform": "posix", "body": "#!/bin/sh\n..." }, "post_install": null, "entries": [ { "path": "res/app.cfg", "mode": 420, "size": 22500, "stored_size": 913, "codec": "gzip", "checksum": "0a77c24c45d0c61f906862c4706f5dd739166985e8fc5c6344dadd9220507b28", "md5": "1520e796ba25554696b3cef6e913cc90", "offset": 8 } ] }

    • size is the original file size, stored_size the space it occupies in the package.
    • checksum is the SHA-256 of the original content, so it validates the data the user actually gets, not the compressed form.
    • md5 is the MD5 fingerprint of the same content. It is a convenience for comparing against what the platform tooling shows, not the integrity guarantee.
    • mode holds UNIX permission bits (0o644 / 0o755); they are ignored on Windows.
    • main_program names the entry that is the application executable.

    A bundle is the same archive appended to an executable. The archive length is manifest_offset + manifest_length + 24, so the archive can be located from the tail of the file without scanning it.

    #Testing

    moon test # unit + integration tests bash scripts/test_linux.sh # end-to-end suite (Linux / WSL)

    scripts/test_linux.sh covers packing, bundling, checksums, the MD5 fingerprints, compression, the executable bit, install-script execution, corruption detection and --skip-scripts. It needs a real POSIX host, so the CI runs it on the Linux runner.

    The unit tests cover the LZ4 implementation as well: the round trip for every shape of payload, the xxHash of the frame header against the value the reference implementation produces, and the interoperability with the lz4 tool in both directions. That last test is skipped where the tool is not installed.

    The tests of the encryption check every primitive against the vector that its standard publishes - FIPS 180-4 for SHA-512, RFC 4231 for HMAC, RFC 5869 for HKDF, RFC 8439 for ChaCha20-Poly1305 and RFC 7748 for X25519 - and the derivation against the reference implementation itself: the master key, the key and the nonce of a record, the check of the key and the sealed record are compared byte for byte with the values that the reference produces.

    #Notes and limitations

    • main must return Unit in this MoonBit version, so the process exit code is always 0. Status is reported on stdout (VERIFY OK / VERIFY FAILED / error: ...); scripts that need an exit code should grep for those markers.
    • UNIX permission bits cannot be set on Windows, so they are only applied on POSIX hosts.
    • The checksum is computed over the original content; verify therefore has to decompress every entry.
    • The fingerprint of the produced file is computed while it is written, so the file is never read back for that. The launcher stub is the one exception: it is hashed after the icon was written into it, because writing the icon changes its bytes.
    • The big integers of the core are not constant time, so the X25519 of this package is not either. The control flow of the ladder does not depend on the secret, but the time of the arithmetic does. For the use here - a key exchange on the machine that holds the secret key - that is not the threat, but it is a reason to look elsewhere when it is.
    • The encryption protects the content of the entries. The names of the entries and their sizes are in the manifest, which is not encrypted.
    • The launcher stub is the LunaPack executable of the machine that built the bundle, so a bundle runs on the platform it was created on. Under moonx (Wasm) no native stub exists, so the bundle falls back to carrying the payload inside the application executable - such a bundle holds the data but does not extract itself.
    • The extraction cache lives next to the bundle (.lunapack/<hash>/). When that folder cannot be written, a temporary directory is used instead.
    • Copying the icon is Windows only, because an icon is a Windows resource.
    • Bundling writes the archive to a temporary file next to the output, so there must be enough free disk space for it.
    • The package is built on moonbitlang/core/async, which the toolchain ships as an experimental package. The MoonBit developers have confirmed that the stable releases will use it, so the package follows the toolchain rather than pinning an older release of the runtime.

    ActiveEncryption

    pub struct ActiveEncryption {
    key : PackageKey
    info : Encryption
    }

    The encryption of a package that is being packed.

    Encryption

    pub struct Encryption {
    method_name : String
    recipient : String
    ephemeral : String
    salt : String
    iterations : Int
    key_check : String
    }

    How a package is encrypted.

    EncryptionOptions

    pub struct EncryptionOptions {
    method_name : String
    public_key : String
    passphrase : String
    iterations : Int
    }

    How a package is encrypted when it is packed.

    Entry

    pub struct Entry {
    path : String
    mode : Int
    size : Int64
    stored_size : Int64
    codec : String
    checksum : String
    md5 : String
    offset : Int64
    }

    One file inside the package.

    Manifest

    pub struct Manifest {
    format_version : Int
    name : String
    version : String
    os : String
    arch : String
    checksum_algo : String
    compression : String
    created_at : String
    main_program : String
    pre_install : Script?
    post_install : Script?
    encryption : Encryption?
    entries : Array[Entry]
    }

    Package metadata plus the list of entries.

    Md5

    pub struct Md5 {
    state : Array[UInt64]
    pending : Array[Byte]
    length : Int64
    }

    The state of an MD5 that is fed in pieces.

    A file that is written while it is hashed does not have to be held in memory, and it never has to be read back to be hashed.

    Md5::Md5

    fn Md5::Md5() -> Md5

    A fresh MD5 computation.

    Md5::finish

    fn Md5::finish(self : Md5) -> String

    The digest of everything that was fed in.

    Md5::update

    fn Md5::update(self : Md5, data : Bytes) -> Unit

    Feed data into the computation.

    PackOptions

    pub struct PackOptions {
    name : String
    version : String
    os : String
    arch : String
    pre_install : Script?
    post_install : Script?
    compression : String
    main_program : String
    encryption : EncryptionOptions?
    }

    Options accepted by pack.

    PackageKey

    pub struct PackageKey {
    master : Bytes
    salt : Bytes
    }

    The key that protects the entries of a package.

    It is derived from the shared secret of the key exchange and never goes into the package.

    PeFile

    pub struct PeFile {
    data : Bytes
    sections : Array[(Int64, Int64, Int64, Int64)]
    resource_rva : Int64
    resource_size : Int64
    }

    A parsed PE image.

    Poly1305

    pub struct Poly1305 {
    r0 : UInt64
    r1 : UInt64
    r2 : UInt64
    r3 : UInt64
    r4 : UInt64
    s0 : UInt64
    s1 : UInt64
    s2 : UInt64
    s3 : UInt64
    h0 : UInt64
    h1 : UInt64
    h2 : UInt64
    h3 : UInt64
    h4 : UInt64
    buffer : Array[Byte]
    }

    Poly1305 (RFC 8439), the one-time authenticator.

    The accumulator is kept as five limbs of 26 bits, which is the shape the reference implementation uses.

    Poly1305::Poly1305

    fn Poly1305::Poly1305(key : Bytes) -> Poly1305

    A fresh Poly1305 computation under the 32-byte key.

    Poly1305::finish

    fn Poly1305::finish(self : Poly1305) -> Bytes

    The tag of everything that was fed in: 16 bytes.

    Poly1305::update

    fn Poly1305::update(self : Poly1305, data : BytesView) -> Unit

    Feed data into the computation.

    Script

    pub struct Script {
    platform : String
    body : String
    }

    An install script together with the platform it targets.

    A package built on Windows can carry a Windows batch script while a package built on Linux carries a POSIX shell script; the matching one is executed at unpack time and the other is skipped.

    SealedPackage

    pub struct SealedPackage {
    shared : Bytes
    ephemeral : Bytes
    recipient : Bytes
    salt : Bytes
    }

    The shared secret of a package that is sealed to the owner of the public key recipient, together with the public numbers of the package.

    Sha512

    pub struct Sha512 {
    state : Array[UInt64]
    pending : Array[Byte]
    length : UInt64
    }

    The state of a SHA-512 that is fed in pieces.

    Sha512::Sha512

    fn Sha512::Sha512() -> Sha512

    A fresh SHA-512 computation.

    Sha512::finish

    fn Sha512::finish(self : Sha512) -> Bytes

    The digest of everything that was fed in: 64 raw bytes.

    Sha512::update

    fn Sha512::update(self : Sha512, data : Bytes) -> Unit

    Feed data into the computation.

    CHECKSUM_ALGO

    let CHECKSUM_ALGO : String

    Checksum algorithm used for every entry payload.

    FORMAT_VERSION

    let FORMAT_VERSION : Int

    Format version recorded in the manifest.

    v3 adds the MD5 fingerprint of every entry next to its SHA-256 checksum. v4 adds the LZ4 codec. v5 adds the encryption of the entries.

    GZIP_LEVEL

    let GZIP_LEVEL : Int

    The compression level that the entries use.

    Level 1 is the fastest setting of the deflate encoder, and it is also the one that produces the smallest packages here: measured on an application of 392 files it compresses faster, decompresses faster and yields a package that is about a quarter smaller than the stream implementation did.

    KEY_ALGORITHM

    let KEY_ALGORITHM : Int

    The algorithm of the protected key file format.

    KEY_BYTES

    let KEY_BYTES : Int

    The number of bytes of a key.

    KEY_HEADER_BYTES

    let KEY_HEADER_BYTES : Int

    The number of bytes of the header of the protected key file.

    KEY_MAGIC

    let KEY_MAGIC : String

    The magic of a protected key file.

    KEY_SALT_BYTES

    let KEY_SALT_BYTES : Int

    The number of bytes of the salt of the protected key file.

    KEY_VERSION

    let KEY_VERSION : Int

    The version of the protected key file format.

    LZ4_BLOCK_SIZE

    let LZ4_BLOCK_SIZE : Int

    The size of the blocks that a frame is cut into.

    LZ4_MAGIC

    let LZ4_MAGIC : UInt

    The magic number that starts every LZ4 frame.

    MAGIC

    let MAGIC : String

    Magic marker at the head and inside the footer of every .appkg file. v2 = SHA-256 checksums + optional gzip compression + install scripts.

    The marker names the container layout, which did not change when the MD5 fingerprint was added: the fingerprint is stored in the manifest and is optional, so a package written by an older version still unpacks and a newer version still reads an older package.

    METHOD_PASSPHRASE

    let METHOD_PASSPHRASE : String

    The method that seals a package with a passphrase.

    METHOD_X25519

    let METHOD_X25519 : String

    The method that seals a package to the owner of a public key.

    NONCE_BYTES

    let NONCE_BYTES : Int

    The number of bytes of the nonce of an entry.

    PASSPHRASE_ITERATIONS

    let PASSPHRASE_ITERATIONS : Int

    The number of iterations of the passphrase derivation.

    RT_GROUP_ICON

    let RT_GROUP_ICON : Int64

    RT_ICON

    let RT_ICON : Int64

    Resource type ids used by the icon resources.

    SALT_BYTES

    let SALT_BYTES : Int

    The number of bytes of the salt of the key derivation.

    SHA512_BYTES

    let SHA512_BYTES : Int

    The length of a SHA-512 digest.

    TAG_BYTES

    let TAG_BYTES : Int

    The length of the tag of the authenticated encryption.

    basename

    fn basename(p : String) -> String

    The base name of a path, without any directory part.

    bundle

    async fn bundle(input_exe : String, output_exe : String, file_dirs : Array[String], opts : PackOptions) -> Unit

    Bundle input_exe together with the contents of every directory in file_dirs into the single file output_exe.

    The result is the original executable with a LunaPack archive appended, so it stays a runnable executable while carrying the extra files. Use unpack on the result to extract the payload again.

    The contents of a single --Files directory land at the root of the archive, next to the main program, so an extracted application is directly runnable. With several directories each one gets a subdirectory named after it, to keep the paths unique.

    bytes_concat

    fn bytes_concat(a : Bytes, b : Bytes) -> Bytes

    The bytes of two buffers, one after the other.

    bytes_to_hex

    fn bytes_to_hex(data : Bytes) -> String

    Lowercase hex of data.

    chacha20_xor

    fn chacha20_xor(key : Bytes, nonce : Bytes, counter : UInt, data : BytesView) -> Bytes

    The ChaCha20 encryption of data: the keystream of the key and the nonce, starting at counter, exclusive-ored with the data.

    chacha20poly1305_open

    fn chacha20poly1305_open(key : Bytes, nonce : Bytes, sealed_data : BytesView, aad : BytesView) -> Bytes?

    Open a ciphertext that was sealed with ChaCha20-Poly1305.

    The sealed data is the ciphertext followed by the 16-byte tag. Returns None when the tag does not match, which means that the data was damaged or that the key is wrong.

    chacha20poly1305_seal

    fn chacha20poly1305_seal(key : Bytes, nonce : Bytes, plaintext : BytesView, aad : BytesView) -> Bytes

    Seal plaintext with ChaCha20-Poly1305.

    The additional data is authenticated but not encrypted. The result is the ciphertext followed by the 16-byte tag.

    collect_files

    async fn collect_files(root : String) -> Array[String]

    Collect every regular file under root, recursively.

    compress_with

    fn compress_with(data : Bytes, codec_name : String) -> (Bytes, String)

    Compress data with the method that method names.

    The codec is one of "none", "gzip", "lz4" or "auto". "auto" keeps the raw bytes when the compression would make the entry bigger, which is what happens with the small files that do not compress at all.

    Returns the payload to store plus the codec name to record in the manifest.

    compute_key_check

    fn compute_key_check(shared : Bytes, recipient : Bytes, ephemeral : Bytes, salt : Bytes) -> Bytes

    The check that tells a wrong key from a damaged package.

    It is a truncated HMAC-SHA512 over the shared secret and the public numbers of the package, so it holds even for a package that carries no entry at all.

    copy_icon

    async fn copy_icon(source_exe : String, target_exe : String) -> Bool

    Copy the icon of source_exe into target_exe.

    The icon is a Windows resource, so this is a no-op elsewhere.

    current_exe_path

    fn current_exe_path() -> String?

    Path of the running executable, when it can be determined.

    default_arch

    fn default_arch() -> String

    Best-effort detection of the host CPU architecture. Windows exposes PROCESSOR_ARCHITECTURE; some POSIX shells export HOSTTYPE.

    derive_package_key

    fn derive_package_key(shared : Bytes, salt : Bytes) -> PackageKey

    Derive the key that protects the entries of a package.

    The input is the shared secret of the key exchange, or the result of the derivation of a passphrase. The result never goes into the package.

    dirname

    fn dirname(p : String) -> String

    The directory part of a path, or "" when the path has no directory.

    ensure_parent_dir

    async fn ensure_parent_dir(path : String) -> Unit

    Create the parent directory of path when it does not exist yet.

    establish_encryption

    async fn establish_encryption(opts : EncryptionOptions) -> ActiveEncryption

    Establish the encryption of a package that is packed.

    The key exchange happens here, before the first entry is written, because the place of an entry in the package is part of its seal.

    extract_icon

    fn extract_icon(pe : PeFile) -> (Int64, Bytes, Array[(Int64, Bytes)])? raise

    The icon of a PE image.

    Returns the id of the group icon, the group icon itself and every icon it references, or None when the image carries no icon.

    extract_to

    async fn extract_to(pkg : String, dest : String, key : PackageKey?) -> Unit

    Extract the payload of pkg into dest silently.

    Used by the self-extracting launcher, which does not want to list every file it writes.

    file_md5_hex

    async fn file_md5_hex(path : String) -> String

    Lowercase hex MD5 of the content of the file at path.

    The file is read as a whole, so it needs to fit in memory; that is the same limitation the packing code has.

    find_resource

    fn find_resource(pe : PeFile, rtype : Int64, name : Int64, lang : Int64) -> (Int64, Int64)? raise

    The payload of a resource, as (offset in the file, size in bytes).

    rtype and name are the numeric resource type and name, lang the language id; use 0 for the first match and a negative name to accept the first entry of that type.

    gzip_compress

    fn gzip_compress(data : Bytes) -> Bytes

    Compress data with gzip.

    gzip_decompress

    fn gzip_decompress(data : Bytes) -> Bytes

    Decompress gzip-compressed data.

    A stream that cannot be decompressed is returned as it is, so that the checksum of the entry reports the damage instead of the program stopping with an error.

    hex_to_bytes

    fn hex_to_bytes(text : String) -> Bytes?

    Decode a hex string.

    Returns None when the text is not a hex string of even length.

    hkdf_expand_sha512

    fn hkdf_expand_sha512(prk : Bytes, info : Bytes, length : Int) -> Bytes

    The HKDF expand step of RFC 5869: OKM = T(1) || T(2) || ...

    T(0) is empty and T(i) = HMAC-Hash(PRK, T(i-1) || info || i), where the counter is a single byte.

    hkdf_extract_sha512

    fn hkdf_extract_sha512(salt : Bytes, ikm : Bytes) -> Bytes

    The HKDF extract step of RFC 5869: PRK = HMAC-Hash(salt, IKM).

    hkdf_sha512

    fn hkdf_sha512(ikm : Bytes, salt : Bytes, info : Bytes, length : Int) -> Bytes

    HKDF-SHA512: the extract step followed by the expand step.

    A salt of zero length is the same as no salt at all: RFC 5869 says that a string of HashLen zeros is used then.

    hmac_sha512

    fn hmac_sha512(key : Bytes, data : Bytes) -> Bytes

    HMAC-SHA512 (RFC 2104) of data under key: 64 raw bytes.

    A key that is longer than the block size is hashed first, a key that is shorter is padded with zeros.

    host_platform

    fn host_platform() -> String

    Name of the platform the program is running on.

    One of "windows", "linux", "macos" or "unknown".

    i64_to_le8

    fn i64_to_le8(v : Int64) -> Bytes

    8-byte little-endian encoding of an Int64 into Bytes.

    is_all_zero

    fn is_all_zero(data : Bytes) -> Bool

    Is the shared secret all zeros?

    A shared secret of all zeros means that the peer sent a point of a small order, which RFC 7748 section 6.1 says to refuse.

    is_native_target

    fn is_native_target() -> Bool

    Is this build a native (directly runnable) target?

    is_protected_key_file

    async fn is_protected_key_file(path : String) -> Bool

    Is the file at path a key file that a passphrase protects?

    iso8601_utc

    fn iso8601_utc(epoch_millis : UInt64) -> String

    Format epoch milliseconds as an ISO-8601 UTC timestamp, e.g. 2026-11-03T14:35:55Z. (@env.now() reports milliseconds.)

    json_to_manifest

    fn json_to_manifest(text : String) -> Manifest raise

    Parse the manifest JSON back into a Manifest.

    le8_to_i64

    fn le8_to_i64(b : Bytes, start : Int) -> Int64

    Read an 8-byte little-endian Int64 from b starting at byte start.

    load_public_key

    async fn load_public_key(path : String) -> Bytes

    Read the public key from path.

    load_secret_key

    async fn load_secret_key(path : String, passphrase : String) -> Bytes

    Read the secret key of a pair from path.

    A file that a passphrase protects needs the passphrase; a file that holds the raw key is read as it is, and the passphrase is not used.

    lz4_compress_frame

    fn lz4_compress_frame(data : Bytes) -> Bytes

    Compress data into a standard LZ4 frame.

    lz4_decompress_frame

    fn lz4_decompress_frame(src : Bytes, size : Int) -> Bytes

    Decompress the LZ4 frame in src.

    size is the number of bytes the frame holds once it is decompressed; the frame format does not carry it, so the caller has to know it.

    magic_bytes

    fn magic_bytes() -> Bytes

    The 8 magic bytes that open the package and close the footer.

    manifest_to_json

    fn manifest_to_json(m : Manifest) -> String

    Serialize a manifest into the canonical UTF-8 JSON representation.

    md5_hex

    fn md5_hex(data : Bytes) -> String

    Lowercase hex MD5 digest of data (32 characters).

    md5_hex_of_string

    fn md5_hex_of_string(s : String) -> String

    Lowercase hex MD5 of the UTF-8 encoding of s.

    new_encryption

    fn new_encryption(method_name : String, recipient : String, ephemeral : String, salt : String, iterations : Int, key_check : String) -> Encryption

    Constructor so callers in other packages can build an Encryption.

    new_encryption_options

    fn new_encryption_options(method_name : String, public_key : String, passphrase : String, iterations : Int) -> EncryptionOptions

    Constructor so callers in other packages can build EncryptionOptions.

    new_pack_options

    fn new_pack_options(name : String, version : String, os : String, arch : String, pre_install : Script?, post_install : Script?, compression : String) -> PackOptions

    Constructor so callers in other packages can build PackOptions without relying on cross-package struct literals (which MoonBit disallows).

    new_package_key

    fn new_package_key(master : Bytes, salt : Bytes) -> PackageKey

    Constructor so callers in other packages can build a PackageKey.

    new_script

    fn new_script(platform : String, body : String) -> Script

    Constructor so other packages can build a Script.

    new_sealed_package

    fn new_sealed_package(shared : Bytes, ephemeral : Bytes, recipient : Bytes, salt : Bytes) -> SealedPackage

    Constructor so callers in other packages can build a SealedPackage.

    norm_sep

    fn norm_sep(p : String) -> String

    Normalize path separators to '/' so host FS calls work uniformly on Windows (which returns '' from realpath) and inside the package format.

    open_entry

    fn open_entry(key : PackageKey, index : Int, path : String, stored : Bytes) -> Bytes?

    Open the payload of the entry with the given index.

    Returns None when the seal does not hold, which means that the key is wrong or that the package was damaged.

    pack

    async fn pack(input : String, output : String, opts : PackOptions) -> Unit

    Bundle every file under input into a .appkg file at output.

    package_tag

    fn package_tag(m : Manifest) -> String

    Short, stable identifier of a package, used to name the cache folder.

    package_tag_of

    fn package_tag_of(text : String) -> String

    Short, stable identifier for arbitrary text.

    parse_pe

    fn parse_pe(data : Bytes) -> PeFile raise

    Parse the headers of a PE image.

    passphrase_secret

    fn passphrase_secret(passphrase : String, salt : Bytes, iterations : Int) -> Bytes

    The shared secret of a package that is sealed with a passphrase.

    pbkdf2_sha512

    fn pbkdf2_sha512(password : Bytes, salt : Bytes, iterations : Int, length : Int) -> Bytes

    PBKDF2-HMAC-SHA512 (RFC 8018): a key of length bytes from a passphrase.

    The result is the concatenation of T(1) || T(2) || ..., where T(i) = U(1) xor U(2) xor ... xor U(iterations) and U(1) = HMAC(password, salt || INT_BE32(i)) and U(j) = HMAC(password, U(j-1)).

    poly1305

    fn poly1305(key : Bytes, data : BytesView) -> Bytes

    The tag of data under the 32-byte key.

    probe_archive

    async fn probe_archive(path : String) -> (Int64, Manifest)?

    Locate the archive inside path, when there is one.

    Returns the offset of the archive and its manifest, or None when the file carries no LunaPack archive.

    random_bytes

    fn random_bytes(count : Int) -> Bytes

    Random bytes from the generator of the core.

    read_at

    fn read_at(pe : PeFile, off : Int64, size : Int64) -> Bytes

    Read size bytes at off from the image.

    read_package_manifest

    async fn read_package_manifest(pkg : String) -> Manifest

    Read the manifest of a package file.

    recover_encryption

    async fn recover_encryption(info : Encryption, secret_key : String, passphrase : String) -> PackageKey

    Recover the key of a package that was sealed.

    The path of the file that holds the secret key is only used by the x25519 method; the passphrase method needs the passphrase.

    relpath

    fn relpath(base : String, full : String) -> String

    Path of full relative to base.

    report_md5

    fn report_md5(path : String, digest : String) -> Unit

    Print a fingerprint that was computed while the file was written.

    The file does not have to be read back for that, which matters when it is a large application.

    report_output_md5

    async fn report_output_md5(path : String) -> Unit

    Print the MD5 fingerprint of the file at path.

    The fingerprint of the produced file is what a user compares against the value the platform tooling shows, so it is printed under a clear label.

    run_install_script

    async fn run_install_script(platform : String, kind : String, script : Script, dest : String) -> Int

    Run an install script of kind inside dest and return its exit status.

    platform selects the interpreter, which makes the POSIX path testable on any host.

    save_public_key

    async fn save_public_key(path : String, public_key : Bytes) -> Unit

    Write the public key of a pair to path.

    save_secret_key

    async fn save_secret_key(path : String, secret_key : Bytes, passphrase : String) -> Unit

    Write the secret key of a pair to path, protected by a passphrase.

    save_secret_key_raw

    async fn save_secret_key_raw(path : String, secret_key : Bytes) -> Unit

    Write the secret key of a pair to path, without a passphrase.

    The key is in clear text on disk, so anyone who reads the file can decrypt every package that was sealed for it. Use the protected form unless there is a reason not to.

    script_applies

    fn script_applies(script_platform : String, host : String) -> Bool

    Does a script targeting script_platform apply on host?

    "any" always applies, "posix" covers Linux and macOS.

    script_platform_of

    fn script_platform_of(path : String) -> String

    Infer the target platform of a script from its file extension.

    seal_entry

    fn seal_entry(key : PackageKey, index : Int, path : String, payload : Bytes) -> Bytes

    Seal the payload of the entry with the given index.

    The result is the ciphertext followed by the 16-byte tag.

    seal_to_public_key

    fn seal_to_public_key(recipient : Bytes) -> SealedPackage

    Establish the shared secret of a package that is sealed to the owner of the public key recipient.

    An ephemeral key is made for every package, so that the same recipient gets a different shared secret for every package.

    set_permissions

    async fn set_permissions(platform : String, path : String, mode : Int) -> Unit

    Set UNIX permission bits on path.

    Windows has no UNIX permission bits and the async runtime's chmod is not implemented there, so the call is skipped on Windows.

    sha256_hex

    fn sha256_hex(data : Bytes) -> String

    Lowercase hex SHA-256 digest of data (64 characters).

    sha256_hex_of_string

    fn sha256_hex_of_string(s : String) -> String

    Lowercase hex SHA-256 of the UTF-8 encoding of s.

    sha512

    fn sha512(data : Bytes) -> Bytes

    The SHA-512 digest of data: 64 raw bytes.

    sha512_hex

    fn sha512_hex(data : Bytes) -> String

    The SHA-512 digest of data, lowercase hex (128 characters).

    shared_with_secret_key

    fn shared_with_secret_key(secret : Bytes, ephemeral : Bytes) -> Bytes

    The shared secret of a package, seen from the side of the recipient.

    shell_for

    fn shell_for(platform : String) -> (String, Array[String], String)

    How a script must be executed on platform: (interpreter, extra arguments, script file extension).

    Windows batch scripts need cmd /c; POSIX shells need plain sh.

    string_to_utf8_bytes

    fn string_to_utf8_bytes(s : String) -> Bytes

    Encode a String into UTF-8 bytes. NOTE: MoonBit strings are UTF-16 internally, so String::to_bytes yields UTF-16 and must not be used for on-disk data. The bundled @utf8 codec gives real UTF-8.

    truncate_bytes

    fn truncate_bytes(data : Bytes, count : Int) -> Bytes

    The first count bytes of data.

    try_launch_self_extracting

    async fn try_launch_self_extracting() -> Bool

    Extract the payload carried by the running executable and start the bundled program.

    Returns false when the executable carries no archive, in which case the caller should behave like an ordinary command line tool.

    unpack

    async fn unpack(pkg : String, dest : String, run_scripts : Bool, key : PackageKey?) -> Unit

    Extract a package into dest.

    run_scripts controls whether the embedded pre_install / post_install scripts are executed.

    utf8_bytes_to_string

    fn utf8_bytes_to_string(bs : Bytes) -> String

    Decode UTF-8 bytes into a String. NOTE: Bytes::to_string() returns the debug representation b'\x..', not the decoded text, so it must not be used here.

    verify

    async fn verify(pkg : String, key : PackageKey?) -> Bool

    Check every entry of a package: magic, sizes, SHA-256 and MD5 digests.

    verify_extracted_dir

    async fn verify_extracted_dir(pkg : String, dir : String) -> Bool

    Check the files in dir against the manifest of pkg.

    This audits what is already on the disk, which is what the user asked for: the files that were extracted must still carry the fingerprint they had in the package. It works on a folder that was filled by unpack and on the cache folder of a self-extracting bundle.

    Returns false when a file is missing or its fingerprint changed.

    with_defaults

    fn with_defaults(opts : PackOptions, name : String, version : String, os : String, arch : String) -> PackOptions

    Return a copy of opts with the given metadata fields replaced.

    with_encryption

    fn with_encryption(opts : PackOptions, encryption : EncryptionOptions) -> PackOptions

    Return a copy of opts that encrypts the package.

    with_main_program

    fn with_main_program(opts : PackOptions, name : String) -> PackOptions

    Return a copy of opts whose main program is name.

    x25519

    fn x25519(scalar : Bytes, point : Bytes) -> Bytes

    The X25519 function of RFC 7748: the shared secret of the private key scalar and the public key point.

    Returns 32 bytes. A shared secret of all zeros means that the peer sent a point of a small order, which the caller has to refuse.

    x25519_public_key

    fn x25519_public_key(scalar : Bytes) -> Bytes

    The public key of the private key scalar: the point with the base point of the curve.

    xxhash32

    fn xxhash32(data : Bytes, start : Int, end : Int, seed : UInt) -> UInt

    XXH32 of data[start..end).

    The LZ4 frame format uses this for the checksum of its header.

    zero_bytes

    fn zero_bytes(count : Int) -> Bytes

    The bytes of count zeros.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io