-
Notifications
You must be signed in to change notification settings - Fork 14
make_release.py
This guide explains how to use the generic MiSTer2MEGA65 (M2M)
make_release.py script to package a core release, and how to adapt the script
to your own core without forking its release logic.
The important idea is this:
make_release.pyis the release engine.CORE/release.tomlis your core-specific identity card.
The Python script knows the standard M2M repository layout and the release
workflow. Your core tells the script only what actually differs between cores:
the public core name, file naming scheme, .cor metadata, config-file name, and
a few release policies.
make_release.py packages files that already exist. It does not run Vivado.
You still synthesize and implement your core first, once per supported MEGA65
board revision.
The script does this:
- Reads
CORE/release.toml. - Optionally imports
CORE/release_hooks.py. - Checks the version name you passed on the command line.
- Checks that the version in
CORE/vhdl/config.vhdmatches, if configured. - For alpha releases, checks
doc/inofficial.mdand verifies the listed git commit. - Selects the requested board revisions.
- Finds the already-built Vivado bitstreams.
- Creates a release folder.
- Copies each
.bitfile into the release folder. - Converts each
.bitinto a.corfile usingcoretoolorbit2core. - Generates the M2M Shell config file using
M2M/tools/make_config.sh. - Copies release notes such as
VERSIONS.mdand, for alpha releases,inofficial.md. - Runs optional core-specific post-packaging hooks.
- Prints a concise summary.
The script does not do this:
- It does not synthesize, implement, or generate bitstreams in Vivado.
- It does not decide whether your core is ready to release.
- It does not write your
VERSIONS.md. - It does not tag git.
- It does not upload to GitHub or to the MEGA65 Filehost.
- It does not create the final Filehost ZIP by itself.
So think of it as the mechanical packaging step between "Vivado bitstreams are ready" and "I am going to publish this release".
The script intentionally hardcodes the M2M conventions that should be the same for every core:
| Purpose | Path or convention |
|---|---|
| Core-specific release config | CORE/release.toml |
| Optional core-specific Python hooks | CORE/release_hooks.py |
| Core configuration VHDL | CORE/vhdl/config.vhd |
| Shell config generator | M2M/tools/make_config.sh |
| R3 bitstream | CORE/CORE-R3.runs/impl_1/mega65_r3.bit |
| R4 bitstream | CORE/CORE-R4.runs/impl_1/mega65_r4.bit |
| R5 bitstream | CORE/CORE-R5.runs/impl_1/mega65_r5.bit |
| R6 bitstream | CORE/CORE-R6.runs/impl_1/mega65_r6.bit |
MEGA65 machine name for .cor conversion |
mega65r3, mega65r4, mega65r5, mega65r6
|
| Release notes | VERSIONS.md |
| Alpha release table | doc/inofficial.md |
This is why CORE/release.toml can stay small. A core maintainer should not
need to describe where Vivado puts the R6 bitstream if the framework already
standardizes that.
The release tool has three layers:
make_release.py CORE/release.toml CORE/release_hooks.py
stable release engine core identity + policy optional custom logic
┌────────────────────┐ ┌────────────────────┐ ┌───────────────────┐
│ parse CLI │ reads │ file_base │ imports │ validate(ctx) │
│ check version │─────────►│ display_name │─────────►│ after_package(ctx)│
│ find bitstreams │ │ .cor metadata │ └───────────────────┘
│ run coretool │ │ config filename │
│ copy release docs │ │ docs policy │
└────────────────────┘ └────────────────────┘
Most cores should only edit CORE/release.toml. Keep CORE/release_hooks.py
empty unless your core genuinely needs an extra check or extra artifact.
The design goal is simple: do not maintain a per-core fork of
make_release.py. If two cores change the Python script independently, the
release process will drift again. If two cores only change CORE/release.toml,
they still share the same tested release engine.
Before you run the script, do the normal release work.
The recommended M2M convention is to keep the version string in exactly one
place in CORE/vhdl/config.vhd:
constant CORE_VERSION : string := "V1";Then derive the user-visible strings and config-file name from that constant:
constant CFG_FILE : string := "/amiga/aexp-" & CORE_VERSION & ".cfg";
constant CORENAME : string := "Amiga 500 for MEGA65 " & CORE_VERSION;The exact directory and filename are core-specific. The pattern matters more than the example: one version constant, used everywhere.
Why this is worth doing:
- the welcome screen shows the same version you package,
- the serial/debug core name shows the same version,
- the
.cormetadata shows the same version, - the generated Shell config file matches the versioned
CFG_FILE, and - the release script can catch accidental mismatches before you publish.
If your core does not yet have a version variable, see section 10.2 below.
VERSIONS.md is what users read to understand what changed, what is known to
work, and what limitations remain. The script can copy it, but it cannot write a
good changelog for you.
For a mature core, VERSIONS.md is normally required. For a very early core,
you may configure it as optional in CORE/release.toml, but that should be a
temporary state.
Alpha versions use this naming scheme:
WIP-V<n>-A<m>
WIP-V<n>-A<m>X<k>
Examples:
WIP-V6-A18
WIP-V1-A3
WIP-V6-A13X1
For these versions, the script normally checks that doc/inofficial.md
contains a row for the alpha and that the commit listed in that row exists in
the local git repository.
This is a useful guard: alpha packages should be traceable to an exact commit.
Open the Vivado project for each board revision you want to ship and generate the bitstream:
./CORE/run_vivado_r3.sh
./CORE/run_vivado_r4.sh
./CORE/run_vivado_r5.sh
./CORE/run_vivado_r6.shThe script expects the generated files here:
CORE/CORE-R3.runs/impl_1/mega65_r3.bit
CORE/CORE-R4.runs/impl_1/mega65_r4.bit
CORE/CORE-R5.runs/impl_1/mega65_r5.bit
CORE/CORE-R6.runs/impl_1/mega65_r6.bit
When packaging all configured boards, the script warns if a bitstream was not built today. That is only a warning, because there are valid reasons to package older bitstreams, but it catches the common mistake "I rebuilt R6 and forgot R3".
The script can use either of the MEGA65 tools:
-
coretool- preferred when available, -
bit2core- fallback.
The tool may be on PATH, or it may be defined by a shell alias in your normal
bash/zsh startup files. The script checks both cases.
If both are available, coretool is used. This matters because coretool
supports the modern metadata flags directly, while bit2core uses its older
tail argument syntax.
Preferably, use coretool. Get it here.
The command shape is:
./make_release.py <version> <output-parent-folder> [targets] [options]Examples:
./make_release.py V1 ~/Desktop
./make_release.py V1.1 /tmp/builds R3,R6
./make_release.py WIP-V6-A18 /tmp/builds R6
./make_release.py A15test1 /tmp/builds R6 --ignore
./make_release.py V1.1 /tmp/builds R3,R4,R5,R6 --force --verbatimThe second argument is a parent folder. The script creates a release subfolder inside it:
<output-parent-folder>/<file_base>-<version>/
For C64MEGA65:
/tmp/builds/C64MEGA65-WIP-V6-A18/
For AExp:
/tmp/builds/AExp-WIP-V1-A3/
Without --ignore, the script accepts these version forms:
| Type | Pattern | Example |
|---|---|---|
| Major release | V<n> |
V6 |
| Minor release | V<n>.<m> |
V6.1 |
| Alpha release | WIP-V<n>-A<m>[X<k>] |
WIP-V6-A18, WIP-V6-A13X1
|
If you pass --ignore, the grammar check is skipped. This is useful for local
test packages:
./make_release.py A15test1 /tmp/builds R6 --ignoreImportant: --ignore does not bypass the configured version-source check.
If CORE/release.toml says that CORE_VERSION must match, then
CORE_VERSION must still be A15test1. This is intentional. It prevents a
test package whose filename says one thing while the core itself reports
another.
If you omit the target argument, all configured boards are packaged:
./make_release.py V1 /tmp/buildsTo package only some boards:
./make_release.py V1 /tmp/builds R6
./make_release.py V1 /tmp/builds R3,R6
./make_release.py V1 /tmp/builds R3+R4+R5+R6Accepted separators are comma, plus, slash, semicolon, and whitespace.
The target names are always the board revisions:
R3 R4 R5 R6
| Option | Meaning |
|---|---|
--force |
Allow writing into an existing non-empty release folder. |
--ignore |
Accept an ad-hoc version name and skip alpha-row checks. |
--verbatim |
Print every step and external command output. |
Without --verbatim, the script keeps the console quiet and prints a final
summary. Use --verbatim while setting up a new core or debugging a release.
For each board, the script copies the .bit file and creates a .cor file.
The names are based on file_base, the version, and the board revision:
<file_base>-<version>-<rev>.bit
<file_base>-<version>-<rev>.cor
For C64MEGA65 WIP-V6-A18 and target R6:
C64MEGA65-WIP-V6-A18-R6.bit
C64MEGA65-WIP-V6-A18-R6.cor
For AExp WIP-V1-A3 and target R3:
AExp-WIP-V1-A3-R3.bit
AExp-WIP-V1-A3-R3.cor
If Shell config generation is enabled, the script also creates the config file
that stores OSM settings persistently. The filename comes from
CORE/release.toml.
For C64MEGA65:
c64mega65-WIP-V6-A18
For AExp:
aexp-WIP-V1-A3.cfg
The file is generated by:
M2M/tools/make_config.sh <output-file> autoThe auto mode reads OPTM_SIZE from CORE/vhdl/config.vhd and writes exactly
that many bytes. This is why the generated config file must be regenerated
whenever the menu layout changes.
The script also copies:
-
VERSIONS.md, depending on policy, -
doc/inofficial.mdfor alpha releases, depending on policy, - any extra files created by
CORE/release_hooks.py.
The .cor file is what users install into a MEGA65 core slot. The script
creates it by calling coretool or bit2core.
The board revision controls the target machine:
| Board | Tool target |
|---|---|
R3 |
mega65r3 |
R4 |
mega65r4 |
R5 |
mega65r5 |
R6 |
mega65r6 |
The public core name and version are written into the .cor metadata:
display_name = "C64 for MEGA65"
version = "WIP-V6-A18"
Some cores also need file association metadata. The C64 core, for example, registers itself as the default handler for cartridge files:
[cor]
bit2core_tail = "=default,c64cart+c64cart"
coretool_flags = "c64cart"
coretool_caps = "c64cart,default"A core that has no MEGA65-registered file type can leave these empty:
[cor]
bit2core_tail = ""
coretool_flags = ""
coretool_caps = ""Do not copy the C64 cartridge metadata blindly. It is correct for the C64 core because C64 cartridge files are meant to be associated with that core. It is wrong for a core whose files are mounted from inside the core's own OSM.
A practical release flow looks like this.
In CORE/vhdl/config.vhd:
constant CORE_VERSION : string := "V1";Make sure CORENAME, welcome/help screens, and CFG_FILE derive from it or
are otherwise manually updated.
Update VERSIONS.md.
For an alpha release, add or update the row in doc/inofficial.md.
Generate the bitstreams in Vivado for every board revision you intend to ship.
For a full modern release, that usually means:
R3, R4, R5, R6
R3 and R3A use the same binary.
Package one board first:
./make_release.py WIP-V1-A3 /tmp/release-smoke R6 --verbatimCheck that:
- the version check passes,
- the expected
.bitfile is copied, - the
.corfile is generated, - the config file has the expected name,
-
VERSIONS.mdis copied, - the final summary looks right.
Install the generated .cor on real hardware through the MEGA65 core slot menu
and boot it the way a user will boot it.
JTAG testing is not a substitute for this. JTAG proves the bitstream can run;
the .cor test proves the release package can be installed and booted.
Run the full package:
./make_release.py V1 /tmp/releasesor explicitly:
./make_release.py V1 /tmp/releases R3,R4,R5,R6The script creates the release folder, not the public ZIP strategy. Before uploading, make sure the package layout makes sense for users:
-
.corfiles for supported boards, -
.bitfiles if you choose to ship them, - generated config file if your core uses persistent settings,
- concise install notes if the download is meant to stand alone,
- links to the full user manual.
Then follow the broader release process:
- tag git,
- create a GitHub release,
- upload to the MEGA65 Filehost,
- announce the release to the community.
That broader process is covered by XYZ. How to release your core to the MEGA65 community.
This is the file a core maintainer normally edits.
[core]
file_base = "C64MEGA65"
display_name = "C64 for MEGA65"
[version]
check = "core_version_constant"
file = "CORE/vhdl/config.vhd"
constant = "CORE_VERSION"
[cor]
bit2core_tail = "=default,c64cart+c64cart"
coretool_flags = "c64cart"
coretool_caps = "c64cart,default"
[shell_config]
enabled = true
filename = "c64mega65-{version}"
[release_notes]
versions_md = "required"
[alpha]
inofficial_md = "required"
[cleanup]
stale_files = ["c64mega65"][core]
file_base = "AExp"
display_name = "Amiga 500 for MEGA65"
[version]
check = "core_version_constant"
file = "CORE/vhdl/config.vhd"
constant = "CORE_VERSION"
[cor]
bit2core_tail = ""
coretool_flags = ""
coretool_caps = ""
[shell_config]
enabled = true
filename = "aexp-{version}.cfg"
[release_notes]
versions_md = "optional"
[alpha]
inofficial_md = "required"
[cleanup]
stale_files = [][core]
file_base = "AExp"
display_name = "Amiga 500 for MEGA65"
boards = ["R3", "R4", "R5", "R6"]file_base is used for the release folder and generated .bit/.cor names:
AExp-WIP-V1-A3/
AExp-WIP-V1-A3-R3.bit
AExp-WIP-V1-A3-R3.cor
display_name is written into the .cor metadata:
Amiga 500 for MEGA65
boards is optional. If omitted, the default is:
boards = ["R3", "R4", "R5", "R6"]Only use a smaller list if your core deliberately supports fewer board revisions.
Recommended:
[version]
check = "core_version_constant"
file = "CORE/vhdl/config.vhd"
constant = "CORE_VERSION"This checks for:
constant CORE_VERSION : string := "V1";and requires it to match the command-line version.
Alternative for non-standard naming:
[version]
check = "regex"
file = "CORE/vhdl/config.vhd"
pattern = 'constant\s+MY_CORE_VERSION\s*:\s*string\s*:=\s*"([^"]+)"'The regex must have one capture group: the version string.
Opt-out:
[version]
check = "none"Use this only for older cores that do not yet expose a version string in VHDL. The script will still package the command-line version, but it will warn that the version was not verified against the core.
[cor]
bit2core_tail = ""
coretool_flags = ""
coretool_caps = ""These values control .cor metadata.
Use empty strings unless your core needs MEGA65 file associations.
For C64 cartridge support:
[cor]
bit2core_tail = "=default,c64cart+c64cart"
coretool_flags = "c64cart"
coretool_caps = "c64cart,default"The script chooses the right syntax depending on whether it runs coretool or
bit2core.
[shell_config]
enabled = true
filename = "aexp-{version}.cfg"If enabled = true, the script creates this file by running:
M2M/tools/make_config.sh <filename> autoThe {version} placeholder is replaced by the command-line version.
Make sure this basename matches the basename in your VHDL CFG_FILE.
Example:
constant CFG_FILE : string := "/amiga/aexp-" & CORE_VERSION & ".cfg";then:
filename = "aexp-{version}.cfg"The generated release artifact is only the file itself:
aexp-V1.cfg
Your user documentation tells the user to put it into:
/amiga/aexp-V1.cfg
If your core does not use persistent OSM settings:
[shell_config]
enabled = false
filename = ""[release_notes]
versions_md = "required"Valid policies:
| Policy | Meaning |
|---|---|
required |
Missing file is a release error. |
optional |
Missing file prints a warning and packaging continues. |
skip |
Do not copy this artifact. |
Use required for mature cores.
[alpha]
inofficial_md = "required"This controls alpha release handling for doc/inofficial.md.
For required, an alpha release such as WIP-V1-A3 must have a row in
doc/inofficial.md, and the commit hash in that row must resolve in git.
For optional, missing rows become warnings. For skip, the check and copy
are disabled.
[cleanup]
stale_files = ["c64mega65"]This removes old generated artifacts from the release folder before writing new
ones. C64MEGA65 uses this to remove the old unversioned c64mega65 config file
when reusing an output folder with --force.
Most new cores can leave this empty:
stale_files = []Use CORE_VERSION in CORE/vhdl/config.vhd:
constant CORE_VERSION : string := "V1";Then derive all user-visible versioned strings from it:
constant CFG_FILE : string := "/mycore/mycore-" & CORE_VERSION & ".cfg";
constant CORENAME : string := "My Core for MEGA65 " & CORE_VERSION;Also use it in welcome and help screens where appropriate:
"My Core for MEGA65 - " & CORE_VERSION & "\n"This makes the release habit one line:
When making a release, update
CORE_VERSION, then package that exact same version.
You have three options.
Best option: add the convention.
This is the cleanest long-term solution. Add:
constant CORE_VERSION : string := "V1";and wire CFG_FILE, CORENAME, welcome screens, and help screens to it.
Transition option: use a custom regex.
If your core already has a version string under a different name:
[version]
check = "regex"
file = "CORE/vhdl/config.vhd"
pattern = 'constant\s+MY_VERSION\s*:\s*string\s*:=\s*"([^"]+)"'Last-resort option: opt out.
[version]
check = "none"The script will package the version from the command line but cannot prove that the core itself reports the same version. This is acceptable for older cores during migration, but it should not be the final state for a maintained M2M core.
Most cores do not need hooks. Keep the file boring:
def validate(ctx):
return None
def after_package(ctx):
return []Hooks are there for genuine core-specific behavior that does not belong in
CORE/release.toml.
validate(ctx) runs after normal preflight checks and before the release folder
is written.
Use it for extra release blockers. Examples:
- check that a mandatory ROM checksum document exists,
- check that a generated handover file exists,
- reject packaging a release from a forbidden branch,
- check that a core-specific binary blob was rebuilt.
Example:
def validate(ctx):
required = ctx.repo / "doc" / "release-checklist.md"
if not required.is_file():
ctx.die("doc/release-checklist.md is missing.")after_package(ctx) runs after the standard package was written. It may create
extra artifacts and return their paths for the final summary.
Example:
def after_package(ctx):
src = ctx.repo / "README.md"
dst = ctx.out / "README.md"
ctx.copy_preserving_timestamps(src, dst)
return [dst]The hook context contains:
| Field | Meaning |
|---|---|
ctx.repo |
Repository root path. |
ctx.out |
Release output folder. |
ctx.version |
Command-line version. |
ctx.kind |
major, minor, alpha, or ignored. |
ctx.targets |
Selected board revisions. |
ctx.config |
Parsed ReleaseConfig. |
ctx.force |
Whether --force was passed. |
ctx.ignore |
Whether --ignore was passed. |
ctx.verbatim |
Whether --verbatim was passed. |
Helper methods:
| Method | Meaning |
|---|---|
ctx.warn(msg) |
Emit a warning and include it in the summary. |
ctx.die(msg) |
Abort the release with an error. |
ctx.copy_preserving_timestamps(src, dst) |
Copy a file like the standard release artifacts. |
Keep hooks small. If a hook grows into a second release script, the abstraction has failed.
For a new core, do this.
Use the current generic make_release.py from an existing M2M core that already
uses CORE/release.toml, for example C64MEGA65 or AExp.
Do not edit the script unless you are improving the shared release engine for all cores.
Start from this minimal template:
[core]
file_base = "MyCore"
display_name = "My Core for MEGA65"
[version]
check = "core_version_constant"
file = "CORE/vhdl/config.vhd"
constant = "CORE_VERSION"
[cor]
bit2core_tail = ""
coretool_flags = ""
coretool_caps = ""
[shell_config]
enabled = true
filename = "mycore-{version}.cfg"
[release_notes]
versions_md = "required"
[alpha]
inofficial_md = "required"
[cleanup]
stale_files = []Change only what is true for your core.
In CORE/vhdl/config.vhd:
constant CORE_VERSION : string := "V1";Derive CFG_FILE and CORENAME from it.
Start empty:
def validate(ctx):
return None
def after_package(ctx):
return []Only add logic when your core genuinely needs it.
Run:
./make_release.py --helpConfirm that the generated help shows your core name, file base, configured boards, and config filename pattern.
After building at least one bitstream:
./make_release.py WIP-V1-A1 /tmp/mycore-release-smoke R6 --ignore --verbatimUse the version that is actually in CORE_VERSION.
If that works, package all boards.
Error:
Version mismatch: command line says 'V1' but CORE/vhdl/config.vhd has 'V0.9'.
Fix: update CORE_VERSION, or call the script with the version that is
actually in the core.
Error:
Missing bitstream(s): CORE/CORE-R6.runs/impl_1/mega65_r6.bit.
Fix: open the matching Vivado project and generate the bitstream.
Error:
'coretool' or 'bit2core' not found.
Fix: install mega65-tools, add the tool to PATH, or define a shell alias
that your bash/zsh startup files can resolve.
Warning:
Some bitstreams were not built today - you may be packaging an outdated release.
Fix: rebuild the listed board if that was not intentional. The script warns but does not abort.
Symptom: OSM settings are not saved or not loaded.
Cause: the generated config file must be exactly OPTM_SIZE bytes. If you add,
remove, or reorder OSM lines, regenerate and redistribute the config file.
The release script does this automatically. Users still need to copy the new
file to the SD-card path named by CFG_FILE.
Symptom: the MEGA65 associates a file type with the wrong core, or a core claims capabilities it should not claim.
Cause: copied [cor] metadata from another core.
Fix: use empty bit2core_tail, coretool_flags, and coretool_caps unless
your core intentionally registers file associations.
C64 has:
[core]
file_base = "C64MEGA65"
display_name = "C64 for MEGA65"
[shell_config]
filename = "c64mega65-{version}"and in config.vhd:
constant CORE_VERSION : string := "WIP-V6-A18";
constant CFG_FILE : string := "/c64/c64mega65-" & CORE_VERSION;After building R6 in Vivado:
./make_release.py WIP-V6-A18 /tmp/release-smoke R6 --verbatimThe output folder is:
/tmp/release-smoke/C64MEGA65-WIP-V6-A18/
It contains:
C64MEGA65-WIP-V6-A18-R6.bit
C64MEGA65-WIP-V6-A18-R6.cor
c64mega65-WIP-V6-A18
VERSIONS.md
inofficial.md
The generated config file basename is c64mega65-WIP-V6-A18. The user places
it in /c64/ on the SD card because CFG_FILE says:
/c64/c64mega65-WIP-V6-A18
AExp has:
[core]
file_base = "AExp"
display_name = "Amiga 500 for MEGA65"
[shell_config]
filename = "aexp-{version}.cfg"and in config.vhd:
constant CORE_VERSION : string := "WIP-V1-A3";
constant CFG_FILE : string := "/amiga/aexp-" & CORE_VERSION & ".cfg";After building R3 in Vivado:
./make_release.py WIP-V1-A3 /tmp/release-smoke R3 --verbatimThe output folder is:
/tmp/release-smoke/AExp-WIP-V1-A3/
It contains:
AExp-WIP-V1-A3-R3.bit
AExp-WIP-V1-A3-R3.cor
aexp-WIP-V1-A3.cfg
VERSIONS.md
inofficial.md
The generated config file basename is aexp-WIP-V1-A3.cfg. The user places it
in /amiga/ on the SD card because CFG_FILE says:
/amiga/aexp-WIP-V1-A3.cfg
If you are maintaining a core, you should normally touch these files for release packaging:
CORE/release.toml
CORE/release_hooks.py only if really needed
CORE/vhdl/config.vhd CORE_VERSION and CFG_FILE convention
VERSIONS.md
doc/inofficial.md for alpha releases
You should normally not touch:
make_release.py
Treat the Python file as shared infrastructure. Improve it when the framework needs a better release engine, not when one core needs a different name.