import datetime as dt
import re
import shutil
import sys
import textwrap
import zipfile
from pathlib import Path
from invoke import task
ROOT = Path(__file__).parent
DIST = ROOT / "dist"
LANG = ROOT / "lang"
def get_version() -> str:
for line in (ROOT / "Cargo.toml").read_text("utf-8").splitlines():
if line.startswith("version ="):
return line.replace("version = ", "").strip('"')
raise RuntimeError("Could not determine version")
def replace_pattern_in_file(file: Path, old: str, new: str, count: int = 1):
content = file.read_text("utf-8")
updated = re.sub(old, new, content, count=count)
file.write_text(updated, "utf-8")
def confirm(prompt: str):
response = input(f"Confirm by typing '{prompt}': ")
if response.lower() != prompt.lower():
sys.exit(1)
@task
def version(ctx):
print(get_version())
@task
def legal(ctx):
version = get_version()
txt_name = f"ludusavi-v{version}-legal.txt"
txt_path = ROOT / "dist" / txt_name
try:
ctx.run(f'cargo lichking bundle --file "{txt_path}"', hide=True)
except Exception:
pass
raw = txt_path.read_text("utf8")
normalized = re.sub(r"C:\\Users\\[^\\]+", "~", raw)
txt_path.write_text(normalized, "utf8")
zip_path = ROOT / "dist" / f"ludusavi-v{version}-legal.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip:
zip.write(txt_path, txt_name)
@task
def flatpak(ctx, generator="/opt/flatpak-cargo-generator.py"):
ctx.run(f'python "{generator}" "{ROOT}/Cargo.lock" -o "{DIST}/generated-sources.json"', hide=True)
@task
def lang(ctx, jar="/opt/crowdin-cli/crowdin-cli.jar"):
ctx.run(f'java -jar "{jar}" pull --export-only-approved')
mapping = {}
for file in LANG.glob("*.ftl"):
if "en-US.ftl" in file.name:
continue
content = file.read_text("utf8")
if content not in mapping:
mapping[content] = set()
mapping[content].add(file)
for group in mapping.values():
if len(group) > 1:
for file in group:
file.unlink()
@task
def clean(ctx):
if DIST.exists():
shutil.rmtree(DIST, ignore_errors=True)
DIST.mkdir()
@task
def docs(ctx):
docs_cli(ctx)
docs_schema(ctx)
@task
def docs_cli(ctx):
docs = Path(__file__).parent / "docs"
if not docs.exists():
docs.mkdir(parents=True)
doc = docs / "cli.md"
commands = [
"--help",
"backup --help",
"restore --help",
"complete --help",
"backups --help",
"find --help",
"manifest --help",
"cloud --help",
"wrap --help",
"api --help",
"schema --help",
]
lines = [
"This is the raw help text for the command line interface.",
]
for command in commands:
print(f"cli.md: {command}")
output = ctx.run(f"cargo run -- {command}", hide=True)
lines.append("")
lines.append(f"## `{command}`")
lines.append("```")
for line in output.stdout.splitlines():
lines.append(line.rstrip())
lines.append("```")
with doc.open("w") as f:
for line in lines:
f.write(line + "\n")
@task
def docs_schema(ctx):
docs = Path(__file__).parent / "docs" / "schema"
if not docs.exists():
docs.mkdir(parents=True)
commands = [
"api-input",
"api-output",
"config",
"general-output",
]
for command in commands:
doc = docs / f"{command}.yaml"
print(f"schema: {command}")
output = ctx.run(f"cargo run -- schema --format yaml {command}", hide=True)
with doc.open("w") as f:
f.write(output.stdout.strip() + "\n")
@task
def prerelease(ctx, new_version, update_lang=True):
date = dt.datetime.now().strftime("%Y-%m-%d")
replace_pattern_in_file(
ROOT / "Cargo.toml",
'version = ".+"',
f'version = "{new_version}"',
)
replace_pattern_in_file(
ROOT / "CHANGELOG.md",
"## Unreleased",
f"## v{new_version} ({date})",
)
replace_pattern_in_file(
ROOT / ".github/ISSUE_TEMPLATE/bug.yaml",
r"(options:)(\n - v\d+\.\d+\.\d+)",
fr"\g<1>\n - v{new_version}\g<2>",
)
replace_pattern_in_file(
ROOT / ".github/ISSUE_TEMPLATE/bug.yaml",
r"- v\d+\.\d+\.\d+\n (- Other)",
fr"\g<1>",
)
for metainfo in [ROOT / "assets/linux/com.mtkennerly.ludusavi.metainfo.xml", ROOT / "assets/flatpak/com.github.mtkennerly.ludusavi.metainfo.xml"]:
replace_pattern_in_file(
metainfo,
"(ludusavi/v).+(/docs/sample-gui-linux.png)",
fr"\g<1>{new_version}\g<2>",
)
replace_pattern_in_file(
metainfo,
"<releases>",
f'<releases>\n <release version="{new_version}" date="{date}"/>',
)
ctx.run("cargo build")
clean(ctx)
legal(ctx)
flatpak(ctx)
docs(ctx)
if update_lang:
lang(ctx)
@task
def release(ctx):
version = get_version()
confirm(f"release {version}")
ctx.run(f'git commit -m "Release v{version}"')
ctx.run(f'git tag v{version} -m "Release"')
ctx.run("git push")
ctx.run(f"git push origin tag v{version}")
@task
def release_flatpak(ctx, target="/git/com.github.mtkennerly.ludusavi"):
target = Path(target)
spec = target / "com.github.mtkennerly.ludusavi.yaml"
version = get_version()
with ctx.cd(target):
ctx.run("git checkout master")
ctx.run("git pull")
ctx.run(f"git checkout -b release/v{version}")
shutil.copy(DIST / "generated-sources.json", target / "generated-sources.json")
spec_content = spec.read_bytes().decode("utf-8")
spec_content = re.sub(r"( tag:) (.*)", fr"\1 v{version}", spec_content)
spec.write_bytes(spec_content.encode("utf-8"))
ctx.run("git add .")
ctx.run(f'git commit -m "Update for v{version}"')
ctx.run("git push origin HEAD")
@task
def release_winget(ctx, target="/git/_forks/winget-pkgs"):
target = Path(target)
version = get_version()
changelog = textwrap.indent(latest_changelog(), " ")
with ctx.cd(target):
ctx.run("git checkout master")
ctx.run("git pull upstream master")
ctx.run(f"git checkout -b mtkennerly.ludusavi-{version}")
ctx.run(f"wingetcreate update mtkennerly.ludusavi --version {version} --urls https://github.com/mtkennerly/ludusavi/releases/download/v{version}/ludusavi-v{version}-win64.zip https://github.com/mtkennerly/ludusavi/releases/download/v{version}/ludusavi-v{version}-win32.zip")
spec = target / f"manifests/m/mtkennerly/ludusavi/{version}/mtkennerly.ludusavi.locale.en-US.yaml"
spec_content = spec.read_bytes().decode("utf-8")
spec_content = spec_content.replace("Moniker: ludusavi", f"Moniker: ludusavi\nReleaseNotes: |-\n{changelog}\nReleaseNotesUrl: https://github.com/mtkennerly/ludusavi/releases/tag/v{version}")
spec.write_bytes(spec_content.encode("utf-8"))
ctx.run(f"winget validate --manifest manifests/m/mtkennerly/ludusavi/{version}")
ctx.run("git add .")
ctx.run(f'git commit -m "mtkennerly.ludusavi version {version}"')
ctx.run("git push origin HEAD")
def latest_changelog() -> str:
changelog = ROOT / "CHANGELOG.md"
content = changelog.read_bytes().decode("utf-8")
lines = []
header = False
for line in content.splitlines():
if line.startswith("#"):
if header:
break
header = True
continue
lines.append(line)
return "\n".join(lines).strip()