Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env python3
#
# This file is part of LiteX.
#
# Copyright (c) 2026 Florent Kermarrec <florent@enjoy-digital.fr>
# SPDX-License-Identifier: BSD-2-Clause
import os
import re
import sys
import json
import glob
import argparse
import tarfile
import zipfile
import shutil
import tempfile
import urllib.error
import urllib.request
import subprocess
import litex_repos
import litex_setup as setup
# Constants ----------------------------------------------------------------------------------------
PYPI_PHASES = ["pypi-check", "pypi-build", "pypi-upload"]
PYPI_DEFAULT_PHASES = ["pypi-check", "pypi-build"]
# Helpers ------------------------------------------------------------------------------------------
def print_banner():
b = []
b.append(" __ _ __ _ __ ")
b.append(" / / (_) /____ | |/_/ ")
b.append(" / /__/ / __/ -_)> < ")
b.append(" /____/_/\\__/\\__/_/|_| ")
b.append(" Build your hardware, easily! ")
b.append(" LiteX Release utility. ")
b.append("")
print("\n".join(b))
def get_current_tag(repo_path):
try:
cmd = ["git", "describe", "--tags", "--abbrev=0"]
result = subprocess.check_output(cmd, cwd=repo_path).decode("UTF-8").strip()
return result
except subprocess.CalledProcessError:
return "No tags"
def get_setup_version(setup_path):
try:
with open(setup_path, "r", encoding="utf-8") as f:
content = f.read()
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if match:
return match.group(1)
return "No version found"
except FileNotFoundError:
return "No setup.py"
def check_release_tag(tag, allow_invalid_tag=False):
if re.match(r"^\d{4}\.(04|08|12)$", tag):
return
if allow_invalid_tag:
return
setup.print_error(f"{tag} is not a valid LiteX release tag.")
print("Expected YYYY.04, YYYY.08 or YYYY.12.")
raise setup.SetupError
def release_repo_names(repos=None, with_pythondata=False):
if repos:
names = [name.strip() for name in repos.split(",") if name.strip()]
else:
names = [
name
for name in litex_repos.install_configs["full"]
if (name != "migen") and (litex_repos.git_repos[name].tag is not None)
]
if with_pythondata:
for name in litex_repos.install_configs["full"]:
if (
(name != "migen") and
name.startswith("pythondata-") and
(name not in names)
):
names.append(name)
for name in names:
if name not in litex_repos.git_repos:
setup.print_error(f"{name} is not a known repository.")
raise setup.SetupError
if name == "migen":
setup.print_error("migen is not released by litex_release.py.")
raise setup.SetupError
return names
def git_output(repo_path, *args, check=True):
cmd = ["git"] + list(args)
try:
output = subprocess.check_output(cmd, cwd=repo_path, stderr=subprocess.STDOUT)
return output.decode("UTF-8").strip()
except subprocess.CalledProcessError as e:
if check:
output = e.output.decode("UTF-8", errors="ignore").strip()
setup.print_error(f"{repo_path}: {' '.join(cmd)} failed.")
if output:
print(output)
raise setup.SetupError
return None
def git_call(repo_path, *args):
cmd = ["git"] + list(args)
subprocess.check_call(cmd, cwd=repo_path)
def normalize_package_name(name):
return re.sub(r"[-_.]+", "-", name).lower()
def repo_build_name(name):
return normalize_package_name(name).replace("-", "_")
def get_setup_metadata(repo_path):
setup_path = os.path.join(repo_path, "setup.py")
if not os.path.exists(setup_path):
return {
"name" : None,
"version" : None,
"error" : "No setup.py",
}
cmd = [sys.executable, "setup.py", "--name", "--version"]
result = subprocess.run(
cmd,
cwd = repo_path,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
)
if result.returncode != 0:
error = result.stderr.decode("UTF-8", errors="ignore").strip()
return {
"name" : None,
"version" : None,
"error" : error or "setup.py metadata query failed",
}
lines = [
line.strip()
for line in result.stdout.decode("UTF-8", errors="ignore").splitlines()
if line.strip()
]
if len(lines) < 2:
return {
"name" : None,
"version" : None,
"error" : "setup.py did not report name and version",
}
return {
"name" : lines[-2],
"version" : lines[-1],
"error" : None,
}
def python_build_available():
result = subprocess.run(
[sys.executable, "-m", "build", "--version"],
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
)
return result.returncode == 0
def python_twine_available():
result = subprocess.run(
[sys.executable, "-m", "twine", "--version"],
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
)
return result.returncode == 0
# Release State ------------------------------------------------------------------------------------
def release_state_filename(tag):
safe_tag = tag.replace("/", "_").replace("\\", "_")
return f".litex_release_{safe_tag}.json"
def init_release_state(tag, phases, states):
repos = []
for state in states:
repo = {
"name" : state["name"],
"path" : state["repo_path"],
"branch" : state["branch"],
"upstream" : state["upstream"],
"last_tag" : state["last_tag"],
"setup_version" : state["setup_version"],
"head_before" : None,
}
if state["exists"]:
repo["head_before"] = git_output(state["repo_path"], "rev-parse", "HEAD", check=False)
repos.append(repo)
return {
"tag" : tag,
"phases" : phases,
"completed_phases" : [],
"repositories" : repos,
"events" : [],
}
def save_release_state(state_file, release_state):
if state_file is None:
return
with open(state_file, "w", encoding="utf-8") as f:
json.dump(release_state, f, indent=2, sort_keys=True)
f.write("\n")
def add_release_event(release_state, state_file, phase, repo, commit=None, tag=None):
if release_state is None:
return
event = {
"phase" : phase,
"repo" : repo,
}
if commit is not None:
event["commit"] = commit
if tag is not None:
event["tag"] = tag
release_state["events"].append(event)
save_release_state(state_file, release_state)
def complete_release_phase(release_state, state_file, phase):
if release_state is None:
return
if phase not in release_state["completed_phases"]:
release_state["completed_phases"].append(phase)
save_release_state(state_file, release_state)
# Release Checks -----------------------------------------------------------------------------------
def release_repo_state(name, tag):
repo_path = os.path.join(setup.current_path, name)
state = {
"name" : name,
"repo_path" : repo_path,
"exists" : os.path.exists(repo_path),
"last_tag" : "Not initialized",
"setup_version" : "Not initialized",
"branch" : "Not initialized",
"expected" : litex_repos.git_repos[name].branch,
"upstream" : None,
"ahead" : None,
"behind" : None,
"dirty" : False,
"push_url" : None,
"local_tag" : False,
"remote_tag" : False,
"remote_tag_check_failed" : False,
}
if not state["exists"]:
return state
state["last_tag"] = get_current_tag(repo_path)
state["setup_version"] = get_setup_version(os.path.join(repo_path, "setup.py"))
state["branch"] = git_output(repo_path, "rev-parse", "--abbrev-ref", "HEAD", check=False) or "unknown"
state["upstream"] = git_output(
repo_path,
"rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}",
check = False,
)
state["dirty"] = bool(git_output(repo_path, "status", "--porcelain", check=False))
state["push_url"] = git_output(repo_path, "remote", "get-url", "--push", "origin", check=False)
state["local_tag"] = git_output(
repo_path,
"rev-parse", "-q", "--verify", f"refs/tags/{tag}",
check = False,
) is not None
remote_tag = git_output(
repo_path,
"ls-remote", "--tags", "origin", f"refs/tags/{tag}",
check = False,
)
state["remote_tag"] = bool(remote_tag)
state["remote_tag_check_failed"] = (remote_tag is None)
if state["upstream"] is not None:
counts = git_output(
repo_path,
"rev-list", "--left-right", "--count", "HEAD...@{u}",
check = False,
)
if counts is not None:
ahead, behind = counts.split()
state["ahead"] = int(ahead)
state["behind"] = int(behind)
return state
def release_actions(state, tag, phases):
actions = []
if "bump" in phases:
if state["setup_version"] in ["No setup.py", "No version found", "Not initialized"]:
actions.append("no-bump")
elif state["setup_version"] == tag:
actions.append("bump-skip")
else:
actions.append("bump")
if "tag" in phases:
actions.append("tag")
if "pypi-check" in phases:
actions.append("pypi-check")
if "pypi-build" in phases:
actions.append("pypi-build")
if "push" in phases:
actions.append("push")
if "pypi-upload" in phases:
actions.append("pypi-upload")
return ",".join(actions) if actions else "check"
def print_release_summary(states, tag, phases):
setup.print_status(f"Release {tag} plan...", underline=True)
print(setup.colorer(
"Repo".ljust(35) +
"Branch".ljust(15) +
"Upstream".ljust(25) +
"Last Tag".ljust(15) +
"Version".ljust(15) +
"Actions"
))
print("-" * 120)
for state in states:
upstream = state["upstream"] or "-"
print(
f"{state['name']:<35} "
f"{state['branch']:<15} "
f"{upstream:<25} "
f"{state['last_tag']:<15} "
f"{state['setup_version']:<15} "
f"{release_actions(state, tag, phases)}"
)
def check_release_state(states, tag, phases, allow_dirty=False, allow_branch_mismatch=False, allow_unpushed=False):
errors = []
for state in states:
name = state["name"]
if not state["exists"]:
errors.append(f"{name}: repository is not initialized.")
continue
if state["dirty"] and not allow_dirty:
errors.append(f"{name}: working tree is not clean.")
if state["branch"] != state["expected"] and not allow_branch_mismatch:
errors.append(f"{name}: on branch {state['branch']}, expected {state['expected']}.")
if state["local_tag"] and "tag" in phases:
errors.append(f"{name}: local tag {tag} already exists.")
if state["remote_tag"] and ("tag" in phases or "push" in phases):
errors.append(f"{name}: remote tag {tag} already exists.")
if "push" in phases:
if state["remote_tag_check_failed"]:
errors.append(f"{name}: could not check remote tag {tag} on origin.")
if state["push_url"] is None:
errors.append(f"{name}: no push URL configured for origin.")
if state["upstream"] is None and not allow_unpushed:
errors.append(f"{name}: no upstream branch configured.")
if state["behind"] not in [None, 0] and not allow_unpushed:
errors.append(f"{name}: branch is behind upstream by {state['behind']} commit(s).")
if state["ahead"] not in [None, 0] and "bump" in phases and not allow_unpushed:
errors.append(f"{name}: branch is ahead of upstream by {state['ahead']} commit(s).")
if "tag" not in phases and not state["local_tag"]:
errors.append(f"{name}: local tag {tag} is missing.")
if "bump" in phases:
setup_path = os.path.join(state["repo_path"], "setup.py")
if (
os.path.exists(setup_path) and
state["setup_version"] == "No version found" and
litex_repos.git_repos[name].tag is not None
):
errors.append(f"{name}: setup.py has no parseable version.")
if errors:
setup.print_error("Release checks failed:")
for error in errors:
print(f" - {error}")
raise setup.SetupError
def release_check_repos(repos=None, with_pythondata=False):
names = release_repo_names(repos=repos, with_pythondata=with_pythondata)
setup.print_status("Checking repositories for release...", underline=True)
print(setup.colorer("Repo".ljust(35) + "Last Tag".ljust(17) + "Setup Version"))
print("-" * 80)
for name in names:
repo_path = os.path.join(setup.current_path, name)
if not os.path.exists(repo_path):
last_tag = "Not initialized"
setup_version = "Not initialized"
else:
last_tag = get_current_tag(repo_path)
setup_version = get_setup_version(os.path.join(repo_path, "setup.py"))
print(f"{name:<35} {last_tag:<15} {setup_version}")
def release_list_repos(repos=None, with_pythondata=False):
names = release_repo_names(repos=repos, with_pythondata=with_pythondata)
setup.print_status("Release repositories...", underline=True)
for name in names:
print(name)
# PyPI Checks/Builds -------------------------------------------------------------------------------
def pypi_release_url(package, version, test_pypi=False):
base = "https://test.pypi.org/pypi" if test_pypi else "https://pypi.org/pypi"
return f"{base}/{normalize_package_name(package)}/{version}/json"
def pypi_release_exists(package, version, test_pypi=False):
request = urllib.request.Request(
pypi_release_url(package, version, test_pypi=test_pypi),
headers = {"User-Agent": "litex-release"},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
return response.status == 200
except urllib.error.HTTPError as e:
if e.code == 404:
return False
raise
def pypi_metadata_for_state(state, tag=None, phases=None):
metadata = get_setup_metadata(state["repo_path"])
if (
(tag is not None) and
(phases is not None) and
("bump" in phases) and
(state["setup_version"] not in ["No setup.py", "No version found", "Not initialized"])
):
metadata = dict(metadata)
metadata["version"] = tag
return metadata
def print_pypi_summary(states, tag=None, phases=None, test_pypi=False):
target = "TestPyPI" if test_pypi else "PyPI"
setup.print_status(f"{target} package plan...", underline=True)
print(setup.colorer(
"Repo".ljust(35) +
"Package".ljust(35) +
"Version"
))
print("-" * 90)
for state in states:
if not state["exists"]:
print(f"{state['name']:<35} {'Not initialized':<35} -")
continue
metadata = pypi_metadata_for_state(state, tag=tag, phases=phases)
package = metadata["name"] or metadata["error"]
version = metadata["version"] or "-"
print(f"{state['name']:<35} {package:<35} {version}")
def check_pypi_state(states, tag=None, phases=None, test_pypi=False, allow_existing=False, check_remote=True):
errors = []
for state in states:
name = state["name"]
if not state["exists"]:
errors.append(f"{name}: repository is not initialized.")
continue
metadata = pypi_metadata_for_state(state, tag=tag, phases=phases)
if metadata["error"] is not None:
errors.append(f"{name}: {metadata['error']}.")
continue
package = metadata["name"]
version = metadata["version"]
if not package or not version:
errors.append(f"{name}: missing package name or version.")
continue
if not check_remote:
continue
try:
exists = pypi_release_exists(package, version, test_pypi=test_pypi)
except Exception as e:
errors.append(f"{name}: could not check {package} {version} on PyPI: {e}.")
continue
if exists and not allow_existing:
errors.append(f"{name}: {package} {version} already exists on PyPI.")
if errors:
setup.print_error("PyPI checks failed:")
for error in errors:
print(f" - {error}")
raise setup.SetupError
def artifact_members(path):
if path.endswith(".whl"):
with zipfile.ZipFile(path, "r") as z:
return z.namelist()
if path.endswith((".tar.gz", ".tgz")):
with tarfile.open(path, "r:gz") as t:
return t.getnames()
return []
def artifact_contains_package(members, package_name, sdist=False):
module_name = package_name.replace("-", "_")
for member in members:
parts = member.split("/")
if sdist:
if len(parts) >= 2 and parts[1] == module_name:
return True
else:
if parts and parts[0] == module_name:
return True
return False
def check_artifact_contents(path, package_name):
members = artifact_members(path)
errors = []
is_sdist = path.endswith((".tar.gz", ".tgz"))
for member in members:
parts = member.split("/")
if member.endswith((".pyc", ".pyo")) or "__pycache__" in parts:
errors.append(f"{path}: contains bytecode/cache file {member}")
break
rel_parts = parts[1:] if is_sdist else parts
if rel_parts and rel_parts[0] in ["bench", "examples", "test", "tests"]:
errors.append(f"{path}: contains top-level {rel_parts[0]} directory")
break
if not artifact_contains_package(members, package_name, sdist=is_sdist):
errors.append(f"{path}: does not contain package module for {package_name}")
if package_name.startswith("pythondata-"):
module_name = package_name.replace("-", "_")
data_files = []
for member in members:
parts = member.split("/")
if is_sdist and len(parts) >= 3 and parts[1] == module_name:
rel = "/".join(parts[2:])
elif (not is_sdist) and len(parts) >= 2 and parts[0] == module_name:
rel = "/".join(parts[1:])
else:
continue
if rel and not rel.endswith(".py") and ".dist-info/" not in member:
data_files.append(member)
if not data_files:
errors.append(f"{path}: pythondata package has no packaged data files")
return errors
def export_repo_tree(repo_path, work_path):
archive = subprocess.Popen(
["git", "archive", "--format=tar", "HEAD"],
cwd = repo_path,
stdout = subprocess.PIPE,
)
extract = subprocess.run(
["tar", "-xf", "-", "-C", work_path],
stdin = archive.stdout,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
)
archive.stdout.close()
archive_rc = archive.wait()
if archive_rc != 0 or extract.returncode != 0:
error = extract.stderr.decode("UTF-8", errors="ignore").strip()
raise setup.SetupError(f"Could not export {repo_path}: {error}")
def build_repo_artifacts(state, dist_root):
name = state["name"]
repo_path = state["repo_path"]
repo_dist = os.path.join(dist_root, repo_build_name(name))
if os.path.exists(repo_dist):
shutil.rmtree(repo_dist)
os.makedirs(repo_dist, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f"litex-release-{name}-") as work_path:
export_repo_tree(repo_path, work_path)
metadata = get_setup_metadata(work_path)
if metadata["error"] is not None:
raise setup.SetupError(f"{name}: {metadata['error']}.")
build_module = python_build_available()
if build_module:
cmd = [
sys.executable, "-m", "build",
"--sdist", "--wheel",
"--outdir", repo_dist,
]
else:
cmd = [
sys.executable, "setup.py",
"sdist", "--dist-dir", repo_dist,
"bdist_wheel", "--dist-dir", repo_dist,
]
setup.print_status(f"Building {name} PyPI artifacts...")
subprocess.check_call(cmd, cwd=work_path)
artifacts = sorted(
glob.glob(os.path.join(repo_dist, "*.tar.gz")) +
glob.glob(os.path.join(repo_dist, "*.whl"))
)
if not artifacts:
raise setup.SetupError(f"{name}: no PyPI artifacts were built.")
errors = []
for artifact in artifacts:
errors += check_artifact_contents(artifact, metadata["name"])
if errors:
setup.print_error(f"{name}: artifact checks failed:")
for error in errors:
print(f" - {error}")
raise setup.SetupError
if python_twine_available():
subprocess.check_call([sys.executable, "-m", "twine", "check"] + artifacts)
else:
setup.print_warning("twine is not installed; skipping twine check.")
return artifacts
def release_pypi_build(states, tag, dist_dir):
dist_root = os.path.abspath(dist_dir or os.path.join(".litex_release_dist", tag))
os.makedirs(dist_root, exist_ok=True)
setup.print_status(f"PyPI artifact directory: {dist_root}")
built = []
for state in states:
built += build_repo_artifacts(state, dist_root)
setup.print_status("Built PyPI artifacts:")
for artifact in built:
print(f" - {artifact}")
return built
def release_pypi_upload(states, tag, dist_dir, test_pypi=False, skip_existing=False):
dist_root = os.path.abspath(dist_dir or os.path.join(".litex_release_dist", tag))
artifacts = []
for state in states:
repo_dist = os.path.join(dist_root, repo_build_name(state["name"]))
artifacts += sorted(glob.glob(os.path.join(repo_dist, "*.tar.gz")))
artifacts += sorted(glob.glob(os.path.join(repo_dist, "*.whl")))
if not artifacts:
raise setup.SetupError(f"No PyPI artifacts found in {dist_root}. Run --pypi-build first.")
cmd = [sys.executable, "-m", "twine", "upload"]
if test_pypi:
cmd += ["--repository", "testpypi"]
if skip_existing:
cmd.append("--skip-existing")
cmd += artifacts
subprocess.check_call(cmd)
# Release ------------------------------------------------------------------------------------------
def release_repos(tag, repos=None, with_pythondata=False, dry_run=False, phases=None, no_push=False,
allow_dirty=False, allow_branch_mismatch=False, allow_unpushed=False, allow_invalid_tag=False,
state_file=None, pypi_dist_dir=None, test_pypi=False, allow_existing_pypi=False,
no_pypi_remote_check=False, pypi_skip_existing=False):
check_release_tag(tag, allow_invalid_tag=allow_invalid_tag)
phases = phases or ["bump", "tag", "push"]
if no_push and "push" in phases:
phases.remove("push")
names = release_repo_names(repos=repos, with_pythondata=with_pythondata)
states = [release_repo_state(name, tag) for name in names]
print_release_summary(states, tag, phases)
check_release_state(
states,
tag,
phases,
allow_dirty = allow_dirty,
allow_branch_mismatch = allow_branch_mismatch,
allow_unpushed = allow_unpushed,
)
if any(phase in phases for phase in PYPI_PHASES):
print_pypi_summary(states, tag=tag, phases=phases, test_pypi=test_pypi)
check_pypi_state(
states,
tag = tag,
phases = phases,
test_pypi = test_pypi,
allow_existing = allow_existing_pypi,
check_remote = not no_pypi_remote_check,
)
if dry_run:
setup.print_status("Dry-run complete, no repository was modified.")
return
if phases == ["pypi-check"]:
setup.print_status("PyPI checks complete.")
return
setup.print_status(f"Making release {tag}...", underline=True)
confirm = input("Please confirm by pressing Y:")
if confirm.upper() != "Y":
setup.print_status("Not confirmed, exiting.")
return
release_state = None
if state_file is not None:
release_state = init_release_state(tag, phases, states)
save_release_state(state_file, release_state)
setup.print_status(f"Release state file: {state_file}")
if "bump" in phases:
for state in states:
name = state["name"]
repo_path = state["repo_path"]
cwd = os.getcwd()
try:
os.chdir(repo_path)
setup_path = os.path.join(repo_path, "setup.py")
if not os.path.exists(setup_path):
setup.print_status(f"No setup.py in {name}, skipping bump.")
continue
current_version = get_setup_version(setup_path)
if current_version == "No version found":
setup.print_status(f"No version in {name} setup.py, skipping bump.")
continue
if current_version == tag:
setup.print_status(f"Version in {name} already at {tag}, skipping bump.")
continue
setup.print_status(f"Bumping version in {name} setup.py from {current_version} to {tag}...")
with open(setup_path, "r", encoding="utf-8") as f:
content = f.read()
new_content = re.sub(r'version\s*=\s*["\'][^"\']+["\']', f'version = "{tag}"', content)
with open(setup_path, "w", encoding="utf-8") as f:
f.write(new_content)
git_call(repo_path, "add", "setup.py")
git_call(repo_path, "commit", "-m", f"Bump to version {tag}")
add_release_event(
release_state,
state_file,
"bump",
name,
commit = git_output(repo_path, "rev-parse", "HEAD"),
)
finally:
os.chdir(cwd)
complete_release_phase(release_state, state_file, "bump")
if "tag" in phases:
for state in states:
name = state["name"]
repo_path = state["repo_path"]
setup.print_status(f"Tagging {name} Git repository as {tag}...")
git_call(repo_path, "tag", tag)
add_release_event(
release_state,
state_file,
"tag",
name,
tag = tag,
)
complete_release_phase(release_state, state_file, "tag")
if "pypi-build" in phases:
release_pypi_build(states, tag, pypi_dist_dir)
complete_release_phase(release_state, state_file, "pypi-build")
if "push" in phases:
for state in states:
name = state["name"]
repo_path = state["repo_path"]
setup.print_status(f"Pushing {name} Git repository...")
git_call(repo_path, "push")
git_call(repo_path, "push", "origin", tag)
add_release_event(
release_state,
state_file,
"push",
name,
commit = git_output(repo_path, "rev-parse", "HEAD"),
tag = tag,
)
complete_release_phase(release_state, state_file, "push")
if "pypi-upload" in phases:
release_pypi_upload(
states,
tag,
pypi_dist_dir,
test_pypi = test_pypi,
skip_existing = pypi_skip_existing,
)
complete_release_phase(release_state, state_file, "pypi-upload")
def release_phases(args):
phases = []
normal_phase_selected = args.bump or args.tag or args.push
pypi_phase_selected = args.pypi or args.pypi_check or args.pypi_build or args.pypi_upload
if args.pypi and not normal_phase_selected:
phases += ["bump", "tag", "push"]
if args.bump:
phases.append("bump")
if args.tag:
phases.append("tag")
if args.push:
phases.append("push")
if args.pypi:
phases += PYPI_DEFAULT_PHASES
if args.pypi_check:
phases.append("pypi-check")
if args.pypi_build:
phases.append("pypi-build")
if args.pypi_upload:
phases.append("pypi-upload")
if not phases and not pypi_phase_selected:
phases = ["bump", "tag", "push"]
if args.no_push and "push" in phases:
phases.remove("push")
if args.no_push and args.push:
setup.print_error("--no-push and --push cannot be used together.")
raise setup.SetupError
deduped = []
for phase in phases:
if phase not in deduped:
deduped.append(phase)
return deduped
# Run ----------------------------------------------------------------------------------------------
def main():
print_banner()
parser = argparse.ArgumentParser()
# Release repositories.
parser.add_argument("--list-repos", action="store_true", help="List repositories selected for release.")
parser.add_argument("--check", action="store_true", help="Check repositories before release.")
parser.add_argument("--repos", default=None, help="Comma-separated release repository allow-list.")
parser.add_argument("--with-pythondata", action="store_true", help="Also include pythondata repositories.")
# Release flow.
parser.add_argument("--release", default=None, help="Make release.")
parser.add_argument("--dry-run", action="store_true", help="Print release plan and checks without modifying repositories.")
parser.add_argument("--no-push", action="store_true", help="Create local release commits/tags without pushing.")
parser.add_argument("--bump", action="store_true", help="Run release version-bump phase.")
parser.add_argument("--tag", action="store_true", help="Run release tag phase.")
parser.add_argument("--push", action="store_true", help="Run release push phase.")
# PyPI flow.
parser.add_argument("--pypi", action="store_true", help="Run PyPI check and build phases.")
parser.add_argument("--pypi-check", action="store_true", help="Check PyPI package metadata and version availability.")
parser.add_argument("--pypi-build", action="store_true", help="Build PyPI sdist/wheel artifacts.")
parser.add_argument("--pypi-upload", action="store_true", help="Upload previously built PyPI artifacts.")
parser.add_argument("--pypi-dist-dir", default=None, help="Directory for PyPI artifacts.")
parser.add_argument("--test-pypi", action="store_true", help="Check/upload against TestPyPI.")
parser.add_argument("--allow-existing-pypi", action="store_true", help="Allow package versions already present on PyPI.")
parser.add_argument("--no-pypi-remote-check", action="store_true", help="Skip PyPI remote version availability checks.")
parser.add_argument("--pypi-skip-existing", action="store_true", help="Pass --skip-existing to twine upload.")
# Release checks.
parser.add_argument("--allow-invalid-tag", action="store_true", help="Allow release tags outside YYYY.04/YYYY.08/YYYY.12.")
parser.add_argument("--allow-dirty", action="store_true", help="Allow dirty working trees during release.")
parser.add_argument("--allow-branch-mismatch", action="store_true", help="Allow branches different from litex_setup.py defaults.")
parser.add_argument("--allow-unpushed", action="store_true", help="Allow repositories without clean upstream synchronization.")
parser.add_argument("--state-file", default=None, help="Release state file path.")
# Development mode.
parser.add_argument("--dev", action="store_true", help=argparse.SUPPRESS)
args = parser.parse_args()
# Location.
setup.litex_setup_location_check()
# List.
if args.list_repos:
release_list_repos(
repos=args.repos,
with_pythondata=args.with_pythondata,
)
# Check.
if args.check:
release_check_repos(
repos=args.repos,
with_pythondata=args.with_pythondata,
)
# Release.
if args.release:
state_file = args.state_file
if (state_file is None) and (not args.dry_run):
state_file = os.path.abspath(release_state_filename(args.release))
release_repos(
tag = args.release,
repos = args.repos,
with_pythondata = args.with_pythondata,
dry_run = args.dry_run,
phases = release_phases(args),
no_push = args.no_push,
allow_dirty = args.allow_dirty,
allow_branch_mismatch = args.allow_branch_mismatch,
allow_unpushed = args.allow_unpushed,
allow_invalid_tag = args.allow_invalid_tag,
state_file = state_file,
pypi_dist_dir = args.pypi_dist_dir,
test_pypi = args.test_pypi,
allow_existing_pypi = args.allow_existing_pypi,
no_pypi_remote_check = args.no_pypi_remote_check,
pypi_skip_existing = args.pypi_skip_existing,
)
elif (
args.dry_run or args.bump or args.tag or args.push or args.no_push or
args.pypi or args.pypi_check or args.pypi_build or args.pypi_upload
):
setup.print_error("--release is required with release action options.")
raise setup.SetupError
if __name__ == "__main__":
main()
d368afb60This file is pinned to the selected release. Download the original to open it in your design tool.