Skip to content

The .mixplugin format

A .mixplugin file is the distributable form of a plugin package — a plain zip of the package folder (the one containing plugin.json), with the folder itself as the zip’s top-level entry. Unzip it next to a destination and you reproduce the package unchanged. There’s no compression trickery and no embedded code execution: packing is a deterministic file copy, and unpacking is a guarded file copy back.

This is the unit the CLI’s pack command builds, link/install consumes, and the marketplace distributes.

pack(pkg_dir, out_path=None, sign_with=None) walks pkg_dir and includes every file except:

  • hidden files/dirs (anything starting with .), including .git
  • __pycache__ directories and .pyc files
  • .DS_Store

Everything else — plugin.json, the entry .py, widgets/, screen/, icons/, and any other package content — is included as-is.

Packing is deterministic on purpose:

  • entries are written in sorted arcname order
  • every entry gets a fixed timestamp (1980-01-01 00:00:00)

So packing the same package folder twice produces a byte-identical .mixplugin, which makes the file itself a stable, hashable artifact (useful for the digest-based trust flow described in Signing your plugin).

Before zipping anything, pack() validates the manifest and raises ValueError if plugin.json doesn’t pass — you can’t pack an invalid package.

Terminal window
mixlar-sdk pack # unsigned .mixplugin next to the package
mixlar-sdk pack --sign my-studio # sign, then pack
mixlar-sdk pack --out dist/my_plugin.mixplugin

If sign_with is given as (key_id, priv_b64), pack() signs the package in place first (rewriting plugin.json’s publisher/sig fields), re-reads the manifest to confirm it’s still valid, and only then zips. That means a signed .mixplugin always has a manifest whose signature matches the exact bytes shipped in the archive.

By default the output filename is <id>-<version>.mixplugin, written next to pkg_dir. Pass out_path (or --out on the CLI) to choose a different location.

Programmatically:

from mixlar import packaging
# unsigned
out = packaging.pack("./my_plugin")
# signed
out = packaging.pack("./my_plugin", sign_with=("my-studio", priv_b64))
# explicit output path
out = packaging.pack("./my_plugin", out_path="dist/my_plugin.mixplugin")

See The mixlar-sdk CLI and Pack, sign, publish for the full command reference.

Inspecting a .mixplugin without extracting it

Section titled “Inspecting a .mixplugin without extracting it”

Two read-only helpers work directly on the zip:

from mixlar import packaging
# just the manifest, parsed as a dict
manifest = packaging.manifest_of("my_plugin-1.0.0.mixplugin")
# every entry (files and folders) inside the archive
entries = packaging.contents("my_plugin-1.0.0.mixplugin")

manifest_of() locates the single plugin.json at the top level of the archive (one path segment above it) and parses it with json.load — no extraction to disk. It raises ValueError if no such manifest entry exists.

contents() is a thin wrapper over zipfile.namelist(), handy for a quick sanity check before you install something.

unpack(mixplugin_path, dest_dir) extracts a .mixplugin into dest_dir and returns the path to the extracted package folder (dest_dir/<pkg_name>).

Before it writes a single byte, unpack() runs path-traversal guards over every entry in the archive:

Check Rejects
Shared top-level folder Archives with more than one top-level folder (ValueError: package has multiple top-level folders)
No absolute paths Any entry name that’s an absolute path
No .. segments Any entry name containing a .. path segment
Resolved path stays inside dest_dir Any entry whose extraction target would land outside dest_dir after normalization
Non-empty archive An archive with no top-level folder at all (ValueError: empty package)

All of these checks happen before zf.extractall() runs — a malicious or corrupted .mixplugin fails loudly instead of writing outside the destination.

from mixlar import packaging
pkg_path = packaging.unpack("my_plugin-1.0.0.mixplugin", "./unpacked")
# -> "./unpacked/my_plugin"

install(mixplugin_path, plugins_dir=None) is unpack() aimed at the app’s default plugin directory:

from mixlar import packaging
installed_path = packaging.install("my_plugin-1.0.0.mixplugin")

Without plugins_dir, it targets %APPDATA%\Mixlar\config\plugins on Windows — the same directory the running app scans for plugins, so an installed package is immediately loadable. Pass plugins_dir explicitly to install somewhere else (useful for testing against a scratch directory).

  1. Write and validate your package (see Packages & the manifest).
  2. mixlar-sdk pack [--sign <key-id>] to produce a .mixplugin.
  3. mixlar-sdk verify if signed, to confirm the signature checks out.
  4. mixlar-sdk link (or packaging.install()) to drop it into the app’s plugins directory for local testing.
CLI command mixlar.packaging function What it does
mixlar-sdk pack pack() Validate, optionally sign, zip deterministically
mixlar-sdk link / install flows install() (→ unpack()) Extract into the app’s plugins directory, guarded against path traversal

See Pack, sign, publish for the command-line walkthrough, and Validate & emulate for checking a package before you pack it.