Inital ver (port)
This commit is contained in:
53
.gitea/workflows/nightly.yml
Normal file
53
.gitea/workflows/nightly.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: nightly-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# 03:30 UTC every night
|
||||||
|
- cron: "30 3 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
only:
|
||||||
|
description: "Space-separated package dirs to build (blank = all)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
replace:
|
||||||
|
description: "Replace existing registry files on version clash"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: archlinux:latest
|
||||||
|
# makepkg refuses to run as root; we create a build user below.
|
||||||
|
options: --privileged
|
||||||
|
steps:
|
||||||
|
- name: Install toolchain
|
||||||
|
run: |
|
||||||
|
pacman -Syu --noconfirm --needed base-devel git python sudo
|
||||||
|
|
||||||
|
- name: Create build user
|
||||||
|
run: |
|
||||||
|
useradd -m -s /bin/bash builder
|
||||||
|
echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder
|
||||||
|
chmod 0440 /etc/sudoers.d/builder
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fix permissions
|
||||||
|
run: chown -R builder:builder "$GITHUB_WORKSPACE"
|
||||||
|
|
||||||
|
- name: Build and publish
|
||||||
|
env:
|
||||||
|
AUTOPKG_REGISTRY_URL: ${{ github.server_url }}
|
||||||
|
AUTOPKG_OWNER: ${{ github.repository_owner }}
|
||||||
|
AUTOPKG_REPO: arch
|
||||||
|
AUTOPKG_USER: ${{ github.actor }}
|
||||||
|
AUTOPKG_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
AUTOPKG_REPLACE: ${{ github.event.inputs.replace }}
|
||||||
|
ONLY: ${{ github.event.inputs.only }}
|
||||||
|
run: |
|
||||||
|
sudo -E -u builder \
|
||||||
|
python3 autopackage.py --keep-going ${ONLY:+--only $ONLY}
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.build/
|
||||||
|
__pycache__/
|
||||||
|
*.pkg.tar.*
|
||||||
|
*.log
|
||||||
134
README.md
Normal file
134
README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# autopackage
|
||||||
|
|
||||||
|
Nightly, directory-driven Arch package builder for Gitea CI.
|
||||||
|
|
||||||
|
Every top-level folder in this repo is a package. The nightly job
|
||||||
|
([`.gitea/workflows/nightly.yml`](.gitea/workflows/nightly.yml)) builds them all
|
||||||
|
in an `archlinux:latest` container and publishes the resulting
|
||||||
|
`*.pkg.tar.zst` files to a [Gitea Arch package
|
||||||
|
registry](https://docs.gitea.com/usage/packages/arch). Gitea keeps the pacman
|
||||||
|
database up to date automatically — there is no `repo-add` step.
|
||||||
|
|
||||||
|
## How a folder is classified
|
||||||
|
|
||||||
|
For a folder `foo/`:
|
||||||
|
|
||||||
|
| Contents of `foo/` | Treated as | What happens |
|
||||||
|
| ----------------------------------------- | ---------- | ------------ |
|
||||||
|
| Contains a `PKGBUILD` | **custom** | Built as-is from the folder. |
|
||||||
|
| No `PKGBUILD` | **AUR** | `https://aur.archlinux.org/foo.git` is cloned, then overrides/patches from `foo/` are applied. |
|
||||||
|
|
||||||
|
### AUR packages
|
||||||
|
|
||||||
|
The folder name **is** the AUR package name. The simplest AUR package is a
|
||||||
|
folder that just contains a `.gitkeep` (git won't track an empty directory):
|
||||||
|
|
||||||
|
```
|
||||||
|
openrgb-git/
|
||||||
|
.gitkeep
|
||||||
|
```
|
||||||
|
|
||||||
|
To customize an AUR package without forking the whole `PKGBUILD`, drop files in
|
||||||
|
the folder:
|
||||||
|
|
||||||
|
- **Patches** — any `*.patch` / `*.diff` files are applied (in sorted order) on
|
||||||
|
top of the freshly cloned AUR repo. `git apply` is tried first, then
|
||||||
|
`patch -Np1`.
|
||||||
|
- **Overrides** — any other file (e.g. an extra source file, a `.install`
|
||||||
|
script) is copied into the clone, replacing a file of the same name.
|
||||||
|
|
||||||
|
> Note: if you add a full `PKGBUILD` to the folder it becomes a *custom*
|
||||||
|
> package and the AUR repo is no longer cloned. Use patches/overrides to tweak
|
||||||
|
> an AUR package.
|
||||||
|
|
||||||
|
Example AUR package with a patch:
|
||||||
|
|
||||||
|
```
|
||||||
|
openrgb-git/
|
||||||
|
.gitkeep
|
||||||
|
0001-fix-udev-rules.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom packages
|
||||||
|
|
||||||
|
A folder with its own `PKGBUILD` (plus any local sources) is built directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-thing/
|
||||||
|
PKGBUILD
|
||||||
|
my-thing.install
|
||||||
|
some-source.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing / consuming
|
||||||
|
|
||||||
|
Packages are uploaded to `{GITEA_URL}/api/packages/{owner}/arch/{repo}` where
|
||||||
|
`{repo}` defaults to `arch` (configurable via `AUTOPKG_REPO`).
|
||||||
|
|
||||||
|
Add this to `/etc/pacman.conf` on client machines (replace the host/owner):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[arch]
|
||||||
|
SigLevel = Optional TrustAll
|
||||||
|
Server = https://git.example.com/api/packages/<owner>/arch/arch/$arch
|
||||||
|
```
|
||||||
|
|
||||||
|
> `SigLevel = Optional TrustAll` is used because packages are unsigned by
|
||||||
|
> default. To sign them, set up a GPG key on the runner and pass `--sign`
|
||||||
|
> (the `.sig` files are uploaded automatically); then you can switch to a
|
||||||
|
> stricter `SigLevel`.
|
||||||
|
|
||||||
|
## CI configuration
|
||||||
|
|
||||||
|
The workflow needs one secret:
|
||||||
|
|
||||||
|
- **`PACKAGE_TOKEN`** — a Gitea access token with the `write:package` scope
|
||||||
|
(and `read:package`). Create it under *Settings → Applications → Generate New
|
||||||
|
Token*, then add it under the repo's *Settings → Actions → Secrets*.
|
||||||
|
|
||||||
|
Everything else is derived from built-in Gitea Actions variables
|
||||||
|
(`GITHUB_SERVER_URL`, `GITHUB_REPOSITORY_OWNER`, `github.actor`).
|
||||||
|
|
||||||
|
The job runs nightly at 03:30 UTC and can also be triggered manually
|
||||||
|
(*workflow_dispatch*) with optional `only` (subset of package dirs) and
|
||||||
|
`replace` inputs.
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run: show what would be built/published
|
||||||
|
./autopackage.py --dry-run
|
||||||
|
|
||||||
|
# Build a subset without publishing
|
||||||
|
./autopackage.py --only openrgb-git paru --no-publish
|
||||||
|
|
||||||
|
# Build everything and publish to a registry
|
||||||
|
AUTOPKG_REGISTRY_URL=https://git.example.com \
|
||||||
|
AUTOPKG_OWNER=joe \
|
||||||
|
AUTOPKG_TOKEN=xxxxxxxx \
|
||||||
|
./autopackage.py --keep-going
|
||||||
|
```
|
||||||
|
|
||||||
|
makepkg must run as a non-root user with `sudo` available for dependency
|
||||||
|
installation. Builds happen in `./.build/` (gitignored).
|
||||||
|
|
||||||
|
### Useful flags
|
||||||
|
|
||||||
|
| Flag | Purpose |
|
||||||
|
| ---- | ------- |
|
||||||
|
| `--only NAME ...` | Build just these package dirs. |
|
||||||
|
| `--no-publish` | Build only; skip uploads. |
|
||||||
|
| `--dry-run` | Print actions without running them. |
|
||||||
|
| `--cleanbuild` | Pass `--cleanbuild` to makepkg. |
|
||||||
|
| `--sign` | Sign packages and upload `.sig` files. |
|
||||||
|
| `--replace` | Delete + re-upload on a version clash (HTTP 409). |
|
||||||
|
| `--keep-going` | Continue if a package fails. |
|
||||||
|
|
||||||
|
Config can also come from env vars: `AUTOPKG_REGISTRY_URL`, `AUTOPKG_OWNER`,
|
||||||
|
`AUTOPKG_REPO`, `AUTOPKG_USER`, `AUTOPKG_TOKEN`, `AUTOPKG_REPLACE`.
|
||||||
|
|
||||||
|
## Ignoring folders
|
||||||
|
|
||||||
|
Top-level dirs `.git`, `.gitea`, `.github`, `.build`, `scripts`, `dist` and
|
||||||
|
`__pycache__` are never treated as packages. Add more (one name per line) in a
|
||||||
|
`.autopackageignore` file at the repo root.
|
||||||
432
autopackage.py
Normal file
432
autopackage.py
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
autopackage.py
|
||||||
|
|
||||||
|
This is a rewrite/port of my original autopackage python script, that i used
|
||||||
|
to just keep to myself but... now im just trusting enough of my personal supply chain
|
||||||
|
that i think im comfy sharing package builds... at least for friends!
|
||||||
|
|
||||||
|
I mostly intend for CI to run this script but, if you want to make tweaks or experiment locally
|
||||||
|
thats super fine too!
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./autopackage.py --dry-run
|
||||||
|
./autopackage.py --only openrgb-git paru
|
||||||
|
./autopackage.py --no-publish # build everything, skip upload
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
AUR_BASE = "https://aur.archlinux.org"
|
||||||
|
|
||||||
|
# Top-level directories that are never packages
|
||||||
|
DEFAULT_IGNORE = {
|
||||||
|
".git",
|
||||||
|
".gitea",
|
||||||
|
".github",
|
||||||
|
".build",
|
||||||
|
"scripts",
|
||||||
|
"dist",
|
||||||
|
"__pycache__",
|
||||||
|
}
|
||||||
|
|
||||||
|
PKG_GLOBS = ("*.pkg.tar.zst", "*.pkg.tar.xz", "*.pkg.tar.gz", "*.pkg.tar.bz2")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
repo_root: Path
|
||||||
|
workdir: Path
|
||||||
|
makepkg_cmd: List[str]
|
||||||
|
sign: bool
|
||||||
|
keep_going: bool
|
||||||
|
dry_run: bool
|
||||||
|
publish: bool
|
||||||
|
replace: bool
|
||||||
|
# Gitea Arch registry settings
|
||||||
|
registry_url: str
|
||||||
|
owner: str
|
||||||
|
arch_repo: str
|
||||||
|
auth_user: str
|
||||||
|
token: str
|
||||||
|
ignore: set = field(default_factory=set)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Shell helpers
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def run(cmd: List[str], cwd: Optional[Path] = None, dry_run: bool = False,
|
||||||
|
check: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
pretty = " ".join(shlex.quote(c) for c in cmd)
|
||||||
|
if cwd:
|
||||||
|
pretty = f"(cd {cwd}) {pretty}"
|
||||||
|
print(f"+ {pretty}", flush=True)
|
||||||
|
if dry_run:
|
||||||
|
return subprocess.CompletedProcess(cmd, 0)
|
||||||
|
return subprocess.run(cmd, cwd=str(cwd) if cwd else None, check=check)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Discovery / classification
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def load_ignore(repo_root: Path) -> set:
|
||||||
|
ignore = set(DEFAULT_IGNORE)
|
||||||
|
ignore_file = repo_root / ".autopackageignore"
|
||||||
|
if ignore_file.is_file():
|
||||||
|
for line in ignore_file.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#"):
|
||||||
|
ignore.add(line)
|
||||||
|
return ignore
|
||||||
|
|
||||||
|
|
||||||
|
def discover_packages(cfg: Config, only: List[str]) -> List[Path]:
|
||||||
|
pkgs: List[Path] = []
|
||||||
|
for entry in sorted(cfg.repo_root.iterdir()):
|
||||||
|
if not entry.is_dir():
|
||||||
|
continue
|
||||||
|
name = entry.name
|
||||||
|
if name.startswith(".") or name in cfg.ignore:
|
||||||
|
continue
|
||||||
|
if only and name not in only:
|
||||||
|
continue
|
||||||
|
pkgs.append(entry)
|
||||||
|
return pkgs
|
||||||
|
|
||||||
|
|
||||||
|
def is_custom(pkg_src: Path) -> bool:
|
||||||
|
return (pkg_src / "PKGBUILD").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Build preparation
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def reset_build_dir(build_dir: Path, dry_run: bool) -> None:
|
||||||
|
if dry_run:
|
||||||
|
print(f"+ rm -rf {build_dir}")
|
||||||
|
return
|
||||||
|
if build_dir.exists():
|
||||||
|
shutil.rmtree(build_dir)
|
||||||
|
build_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_custom(pkg_src: Path, build_dir: Path, dry_run: bool) -> None:
|
||||||
|
print(f" custom package -> copying sources into {build_dir}")
|
||||||
|
if dry_run:
|
||||||
|
print(f"+ cp -r {pkg_src} {build_dir}")
|
||||||
|
return
|
||||||
|
shutil.copytree(pkg_src, build_dir, ignore=shutil.ignore_patterns(".git"))
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_aur(pkg_src: Path, build_dir: Path, cfg: Config) -> None:
|
||||||
|
name = pkg_src.name
|
||||||
|
aur_url = f"{AUR_BASE}/{name}.git"
|
||||||
|
print(f" AUR package -> cloning {aur_url}")
|
||||||
|
run(["git", "clone", "--depth", "1", aur_url, str(build_dir)], dry_run=cfg.dry_run)
|
||||||
|
apply_overrides(pkg_src, build_dir, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_overrides(pkg_src: Path, build_dir: Path, cfg: Config) -> None:
|
||||||
|
patches = sorted(
|
||||||
|
p for p in pkg_src.iterdir()
|
||||||
|
if p.is_file() and p.suffix in (".patch", ".diff")
|
||||||
|
)
|
||||||
|
overrides = sorted(
|
||||||
|
p for p in pkg_src.iterdir()
|
||||||
|
if p.is_file()
|
||||||
|
and p.suffix not in (".patch", ".diff")
|
||||||
|
and p.name != ".gitkeep"
|
||||||
|
)
|
||||||
|
|
||||||
|
for ov in overrides:
|
||||||
|
dest = build_dir / ov.name
|
||||||
|
print(f" override: {ov.name}")
|
||||||
|
if cfg.dry_run:
|
||||||
|
print(f"+ cp {ov} {dest}")
|
||||||
|
continue
|
||||||
|
shutil.copy2(ov, dest)
|
||||||
|
|
||||||
|
for patch in patches:
|
||||||
|
print(f" patch: {patch.name}")
|
||||||
|
apply_patch(patch, build_dir, cfg.dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_patch(patch: Path, build_dir: Path, dry_run: bool) -> None:
|
||||||
|
# Use `git apply` first if theres a patch that i have in there
|
||||||
|
git_apply = run(
|
||||||
|
["git", "apply", "--whitespace=nowarn", str(patch)],
|
||||||
|
cwd=build_dir, dry_run=dry_run, check=False,
|
||||||
|
)
|
||||||
|
if git_apply.returncode == 0:
|
||||||
|
return
|
||||||
|
if dry_run:
|
||||||
|
return
|
||||||
|
print(f" (git apply failed, trying patch -p1) {patch.name}")
|
||||||
|
with patch.open("rb") as fh:
|
||||||
|
result = subprocess.run(
|
||||||
|
["patch", "-Np1", "--forward"],
|
||||||
|
cwd=str(build_dir), stdin=fh,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"failed to apply patch {patch.name}")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Build
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def find_artifacts(build_dir: Path, want_sig: bool) -> List[Path]:
|
||||||
|
found: List[Path] = []
|
||||||
|
for pat in PKG_GLOBS:
|
||||||
|
found.extend(build_dir.glob(pat))
|
||||||
|
pkgs = [p for p in found if p.is_file() and not p.name.endswith(".sig")]
|
||||||
|
pkgs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
|
artifacts: List[Path] = []
|
||||||
|
for p in pkgs:
|
||||||
|
artifacts.append(p)
|
||||||
|
if want_sig:
|
||||||
|
sig = p.with_name(p.name + ".sig")
|
||||||
|
if sig.is_file():
|
||||||
|
artifacts.append(sig)
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def build_one(pkg_src: Path, cfg: Config) -> List[Path]:
|
||||||
|
name = pkg_src.name
|
||||||
|
build_dir = cfg.workdir / name
|
||||||
|
print(f"\n=== {name} ===")
|
||||||
|
|
||||||
|
reset_build_dir(build_dir, cfg.dry_run)
|
||||||
|
if is_custom(pkg_src):
|
||||||
|
prepare_custom(pkg_src, build_dir, cfg.dry_run)
|
||||||
|
else:
|
||||||
|
prepare_aur(pkg_src, build_dir, cfg)
|
||||||
|
|
||||||
|
run(cfg.makepkg_cmd, cwd=build_dir, dry_run=cfg.dry_run)
|
||||||
|
|
||||||
|
if cfg.dry_run:
|
||||||
|
return []
|
||||||
|
|
||||||
|
artifacts = find_artifacts(build_dir, want_sig=cfg.sign)
|
||||||
|
pkg_files = [a for a in artifacts if not a.name.endswith(".sig")]
|
||||||
|
if not pkg_files:
|
||||||
|
raise RuntimeError(f"{name}: build finished but no *.pkg.tar.* produced")
|
||||||
|
print(f" built: {', '.join(a.name for a in pkg_files)}")
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Publish to Gitea Arch registry
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def parse_pkg_filename(filename: str) -> Optional[tuple]:
|
||||||
|
"""Return (name, version, arch) from <name>-<ver>-<rel>-<arch>.pkg.tar.zst."""
|
||||||
|
base = filename
|
||||||
|
for suffix in (".pkg.tar.zst", ".pkg.tar.xz", ".pkg.tar.gz", ".pkg.tar.bz2"):
|
||||||
|
if base.endswith(suffix):
|
||||||
|
base = base[: -len(suffix)]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
parts = base.rsplit("-", 3)
|
||||||
|
if len(parts) != 4:
|
||||||
|
return None
|
||||||
|
name, ver, rel, arch = parts
|
||||||
|
return name, f"{ver}-{rel}", arch
|
||||||
|
|
||||||
|
|
||||||
|
def registry_base(cfg: Config) -> str:
|
||||||
|
return f"{cfg.registry_url.rstrip('/')}/api/packages/{cfg.owner}/arch/{cfg.arch_repo}"
|
||||||
|
|
||||||
|
|
||||||
|
def auth_header(cfg: Config) -> str:
|
||||||
|
raw = f"{cfg.auth_user}:{cfg.token}".encode("utf-8")
|
||||||
|
return "Basic " + base64.b64encode(raw).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def http_request(url: str, method: str, cfg: Config,
|
||||||
|
data: Optional[bytes] = None) -> tuple:
|
||||||
|
req = urllib.request.Request(url, data=data, method=method)
|
||||||
|
req.add_header("Authorization", auth_header(cfg))
|
||||||
|
if data is not None:
|
||||||
|
req.add_header("Content-Type", "application/octet-stream")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return resp.status, resp.read().decode("utf-8", "replace")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.read().decode("utf-8", "replace")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_version(filename: str, cfg: Config) -> None:
|
||||||
|
meta = parse_pkg_filename(filename)
|
||||||
|
if not meta:
|
||||||
|
return
|
||||||
|
name, version, arch = meta
|
||||||
|
url = f"{registry_base(cfg)}/{name}/{version}/{arch}"
|
||||||
|
status, body = http_request(url, "DELETE", cfg)
|
||||||
|
print(f" delete {name} {version} {arch} -> {status}")
|
||||||
|
|
||||||
|
|
||||||
|
def publish_file(path: Path, cfg: Config) -> None:
|
||||||
|
url = registry_base(cfg)
|
||||||
|
if cfg.dry_run:
|
||||||
|
print(f"+ PUT {url} ({path.name})")
|
||||||
|
return
|
||||||
|
data = path.read_bytes()
|
||||||
|
status, body = http_request(url, "PUT", cfg, data=data)
|
||||||
|
if status in (200, 201):
|
||||||
|
print(f" uploaded {path.name} -> {status}")
|
||||||
|
return
|
||||||
|
if status == 409 and cfg.replace:
|
||||||
|
print(f" {path.name} exists; replacing")
|
||||||
|
delete_version(path.name, cfg)
|
||||||
|
status, body = http_request(url, "PUT", cfg, data=data)
|
||||||
|
if status in (200, 201):
|
||||||
|
print(f" uploaded {path.name} -> {status}")
|
||||||
|
return
|
||||||
|
if status == 409:
|
||||||
|
print(f" {path.name} already published (409); skipping "
|
||||||
|
f"(use --replace to overwrite)")
|
||||||
|
return
|
||||||
|
raise RuntimeError(
|
||||||
|
f"upload of {path.name} failed: HTTP {status}: {body.strip()[:500]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def publish(artifacts: List[Path], cfg: Config) -> None:
|
||||||
|
for art in artifacts:
|
||||||
|
publish_file(art, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# CLI
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def env(name: str, default: str = "") -> str:
|
||||||
|
return os.environ.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
|
def build_config(args: argparse.Namespace) -> Config:
|
||||||
|
repo_root = args.repo_root.resolve()
|
||||||
|
|
||||||
|
makepkg_cmd = ["makepkg", "-s", "--noconfirm", "--needed"]
|
||||||
|
if args.cleanbuild:
|
||||||
|
makepkg_cmd.append("--cleanbuild")
|
||||||
|
if args.sign:
|
||||||
|
makepkg_cmd.append("--sign")
|
||||||
|
|
||||||
|
cfg = Config(
|
||||||
|
repo_root=repo_root,
|
||||||
|
workdir=args.workdir.resolve(),
|
||||||
|
makepkg_cmd=makepkg_cmd,
|
||||||
|
sign=args.sign,
|
||||||
|
keep_going=args.keep_going,
|
||||||
|
dry_run=args.dry_run,
|
||||||
|
publish=not args.no_publish,
|
||||||
|
replace=args.replace,
|
||||||
|
registry_url=args.registry_url,
|
||||||
|
owner=args.owner,
|
||||||
|
arch_repo=args.arch_repo,
|
||||||
|
auth_user=args.auth_user or args.owner,
|
||||||
|
token=args.token,
|
||||||
|
ignore=load_ignore(repo_root),
|
||||||
|
)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: List[str]) -> int:
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description="Nightly directory-driven Arch package builder for Gitea CI",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
ap.add_argument("--repo-root", type=Path, default=Path.cwd(),
|
||||||
|
help="Repo root to scan for package directories")
|
||||||
|
ap.add_argument("--workdir", type=Path, default=Path.cwd() / ".build",
|
||||||
|
help="Scratch directory for clones/builds")
|
||||||
|
ap.add_argument("--only", nargs="*", default=[],
|
||||||
|
help="Only build these package directory names")
|
||||||
|
ap.add_argument("--cleanbuild", action="store_true",
|
||||||
|
help="Pass --cleanbuild to makepkg")
|
||||||
|
ap.add_argument("--sign", action="store_true",
|
||||||
|
help="Sign packages (makepkg --sign) and upload .sig files")
|
||||||
|
ap.add_argument("--keep-going", action="store_true",
|
||||||
|
help="Continue with remaining packages if one fails")
|
||||||
|
ap.add_argument("--dry-run", action="store_true",
|
||||||
|
help="Print actions without executing them")
|
||||||
|
ap.add_argument("--no-publish", action="store_true",
|
||||||
|
help="Build only; do not upload to the registry")
|
||||||
|
ap.add_argument("--replace", action="store_true",
|
||||||
|
default=env("AUTOPKG_REPLACE", "") not in ("", "0", "false"),
|
||||||
|
help="Delete + re-upload when a file already exists (409)")
|
||||||
|
|
||||||
|
# Gitea Arch registry (CLI overrides env; env defaults suit Gitea Actions)
|
||||||
|
ap.add_argument("--registry-url", default=env("AUTOPKG_REGISTRY_URL",
|
||||||
|
env("GITHUB_SERVER_URL")),
|
||||||
|
help="Gitea base URL, e.g. https://git.example.com")
|
||||||
|
ap.add_argument("--owner", default=env("AUTOPKG_OWNER",
|
||||||
|
env("GITHUB_REPOSITORY_OWNER")),
|
||||||
|
help="Package registry owner (user or org)")
|
||||||
|
ap.add_argument("--arch-repo", default=env("AUTOPKG_REPO", "arch"),
|
||||||
|
help="Registry repository/group name (the [repo] in pacman.conf)")
|
||||||
|
ap.add_argument("--auth-user", default=env("AUTOPKG_USER",
|
||||||
|
env("GITHUB_ACTOR")),
|
||||||
|
help="Username for HTTP basic auth (defaults to owner)")
|
||||||
|
ap.add_argument("--token", default=env("AUTOPKG_TOKEN",
|
||||||
|
env("PACKAGE_TOKEN", env("GITHUB_TOKEN"))),
|
||||||
|
help="Gitea token with write:package scope")
|
||||||
|
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
cfg = build_config(args)
|
||||||
|
|
||||||
|
pkgs = discover_packages(cfg, args.only)
|
||||||
|
if not pkgs:
|
||||||
|
print("No package directories found.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print(f"Discovered {len(pkgs)} package(s): "
|
||||||
|
f"{', '.join(p.name for p in pkgs)}")
|
||||||
|
|
||||||
|
if cfg.publish and not cfg.dry_run:
|
||||||
|
missing = [n for n, v in (("registry-url", cfg.registry_url),
|
||||||
|
("owner", cfg.owner),
|
||||||
|
("token", cfg.token)) if not v]
|
||||||
|
if missing:
|
||||||
|
print(f"Cannot publish: missing {', '.join(missing)}. "
|
||||||
|
f"Set the matching env/secret or pass --no-publish.",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
failures: List[str] = []
|
||||||
|
for pkg_src in pkgs:
|
||||||
|
try:
|
||||||
|
artifacts = build_one(pkg_src, cfg)
|
||||||
|
if cfg.publish and not cfg.dry_run:
|
||||||
|
publish(artifacts, cfg)
|
||||||
|
elif cfg.publish and cfg.dry_run:
|
||||||
|
print(f" (dry-run) would publish artifacts for {pkg_src.name}")
|
||||||
|
except (subprocess.CalledProcessError, RuntimeError, OSError) as e:
|
||||||
|
print(f"✗ {pkg_src.name}: {e}", file=sys.stderr)
|
||||||
|
failures.append(pkg_src.name)
|
||||||
|
if not cfg.keep_going:
|
||||||
|
break
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
print(f"\nFailed: {', '.join(failures)}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("\nAll done.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
0
bacula/.gitkeep
Normal file
0
bacula/.gitkeep
Normal file
0
fchat-horizon-appimage/.gitkeep
Normal file
0
fchat-horizon-appimage/.gitkeep
Normal file
0
ggml-git-latest-commit/.gitkeep
Normal file
0
ggml-git-latest-commit/.gitkeep
Normal file
0
openrgb-git/.gitkeep
Normal file
0
openrgb-git/.gitkeep
Normal file
0
paru/.gitkeep
Normal file
0
paru/.gitkeep
Normal file
19
plezy-bin/0001-sentry-native-inproc-backend.patch
Normal file
19
plezy-bin/0001-sentry-native-inproc-backend.patch
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
|
||||||
|
index 29b0af464..721fa7864 100644
|
||||||
|
--- a/linux/CMakeLists.txt
|
||||||
|
+++ b/linux/CMakeLists.txt
|
||||||
|
@@ -81,6 +81,14 @@ set_target_properties(${BINARY_NAME}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
+# Build sentry-native with the in-process backend instead of crashpad. The
|
||||||
|
+# crashpad backend forks a handler and calls setsid(), which fails (EPERM) when
|
||||||
|
+# plezy is launched as a systemd app scope / session leader, tripping a fatal
|
||||||
|
+# CHECK in crashpad that SIGTRAPs on startup. inproc installs in-process signal
|
||||||
|
+# handlers and avoids the fork/setsid path. Overrides the plugin's forced
|
||||||
|
+# SENTRY_BACKEND=crashpad via the env-var escape hatch in sentry-native.cmake.
|
||||||
|
+set(ENV{SENTRY_NATIVE_BACKEND} "inproc")
|
||||||
|
+
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
0
qownnotes/.gitkeep
Normal file
0
qownnotes/.gitkeep
Normal file
0
ventoy-bin/.gitkeep
Normal file
0
ventoy-bin/.gitkeep
Normal file
0
visual-studio-code-bin/.gitkeep
Normal file
0
visual-studio-code-bin/.gitkeep
Normal file
Reference in New Issue
Block a user