commit 3d480b48f301eead29407449e73ad3f4bd8bc58d Author: sisungo Date: Mon Jun 8 16:43:26 2026 +0800 Initial commit Signed-off-by: sisungo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f976cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/packages/*/target +__pycache__ +/.vscode +/.idea +/.zed diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 0000000..df10de0 --- /dev/null +++ b/hooks/__init__.py @@ -0,0 +1,9 @@ +from typing import Callable, TypeAlias + +from . import autostrip + +PostBuild: TypeAlias = Callable[[], None] + +post_build: dict[str, PostBuild] = { + "autostrip": autostrip.autostrip, +} diff --git a/hooks/autostrip.py b/hooks/autostrip.py new file mode 100644 index 0000000..0153027 --- /dev/null +++ b/hooks/autostrip.py @@ -0,0 +1,2 @@ +def autostrip(): + pass diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/arch.py b/lib/arch.py new file mode 100644 index 0000000..9f73c67 --- /dev/null +++ b/lib/arch.py @@ -0,0 +1,19 @@ +import os + +from . import packie + +all: dict[str, dict[str, str]] = { + "aarch64-semios-linux": {}, +} +_target: str = packie.host_arch() + + +def get_target(): + return _target + + +def set_target(arch: str): + global _target + _target = arch + for key, val in all[_target]: + os.environ[key] = val diff --git a/lib/cpp.py b/lib/cpp.py new file mode 100644 index 0000000..4b0c955 --- /dev/null +++ b/lib/cpp.py @@ -0,0 +1,62 @@ +import os +import subprocess + +from . import make as libmake +from . import pkgcomposer + + +def configure(rem: list[str] = []): + subprocess.run(["./configure"] + rem, check=True) + + +def make(rem: list[str] = []): + subprocess.run(["make", f"-j{os.cpu_count()}"] + rem, check=True) + + +def make_install(rem: list[str] | None = None, chdir: bool = True): + default_rem = ["DESTDIR=" + os.getcwd() + "/../cpp_install"] + subprocess.run(["make", "install"] + (rem or default_rem), check=True) + if rem is None and chdir: + os.chdir("../cpp_install") + + +def _do_auto_pack(pc: pkgcomposer.PkgComposer, srcdir: str, dstdir: str, filter): + pc.makedir(srcdir) + with os.scandir(srcdir) as entries: + for ent in entries: + if filter(ent): + if ent.is_dir(follow_symlinks=False): + pc.makedir(f"{dstdir}/{ent.name}") + else: + pc.addfile(ent.path, f"{dstdir}/{ent.name}") + + +def _autopack_filter_lib(ent) -> bool: + if ent.name.endswith(".o") or ent.name.endswith(".a"): + return False + return True + + +def _autopack_filter_bin(ent) -> bool: + return True + + +def _autopack_filter_dev(ent): + if ".so" in ent.name: + return False + return True + + +def _auto_pack(profiles: list[str], pc: pkgcomposer.PkgComposer): + if "lib" in profiles: + _do_auto_pack(pc, "lib", "lib", _autopack_filter_lib) + if "bin" in profiles: + _do_auto_pack(pc, "bin", "bin", _autopack_filter_bin) + _do_auto_pack(pc, "sbin", "bin", _autopack_filter_bin) + if "dev" in profiles: + _do_auto_pack(pc, "lib", "lib", _autopack_filter_dev) + _do_auto_pack(pc, "include", "include", _autopack_filter_dev) + + +def use_auto_pack(profiles: list[str]): + libmake.on_pack(lambda pc: _auto_pack(profiles, pc)) diff --git a/lib/make.py b/lib/make.py new file mode 100644 index 0000000..9f41a54 --- /dev/null +++ b/lib/make.py @@ -0,0 +1,61 @@ +import typing + +from . import pkgcomposer, sources + +_key_global = "@global" +_key_version = "version" +_key_description = "description" +_key_on_pack = "on_pack" +_key_arch_any = "arch_any" + +_current_pkgname: str = _key_global +_pkginfo: dict[str, dict[str, typing.Any]] = { + _key_global: {}, +} +_supported_arch: typing.Callable[[str], bool] = lambda x: True +_on_build: typing.Callable[[], None] | None = None + +_blocked_hooks: list[str] = [] + + +def package(s: str): + global _current_pkgname + _current_pkgname = s + _pkginfo[s] = _pkginfo[_key_global].copy() + + +def version(s: str): + _pkginfo[_current_pkgname][_key_version] = s + + +def description(s: str): + _pkginfo[_current_pkgname][_key_description] = s + + +def arch_any(): + _pkginfo[_current_pkgname][_key_arch_any] = True + + +def supported_arch(s: list[str] | typing.Callable[[str], bool]): + global _supported_arch + if isinstance(s, list): + _supported_arch = lambda arch: arch in s + else: + _supported_arch = s + + +def on_build(func: typing.Callable[[], None]): + global _on_build + _on_build = func + + +def on_pack(func: typing.Callable[[pkgcomposer.PkgComposer], None]): + _pkginfo[_current_pkgname][_key_on_pack] = func + + +def block_hook(hook: str): + _blocked_hooks.append(hook) + + +def source_url(url: str): + sources.source_url(url) diff --git a/lib/packie.py b/lib/packie.py new file mode 100644 index 0000000..5ed847f --- /dev/null +++ b/lib/packie.py @@ -0,0 +1,11 @@ +import subprocess +import os + +PACKIE_EXEC = os.getenv("PACKIE", "packie") + +def host_arch() -> str: + result = subprocess.run( + [PACKIE_EXEC, "print", "profile.host_arch"], + check=True, capture_output=True, + ) + return result.stdout.decode().strip() diff --git a/lib/pkgcomposer.py b/lib/pkgcomposer.py new file mode 100644 index 0000000..be2b085 --- /dev/null +++ b/lib/pkgcomposer.py @@ -0,0 +1,39 @@ +import json +import os +import shutil +import sys + +if sys.version_info >= (3, 14): + _archive_format = "zstdtar" + _archive_suffix = ".tar.zst" +else: + _archive_format = "gztar" + _archive_suffix = ".tar.gz" + + +class PkgComposer: + def __init__(self, pkgfile: str): + self.pkgfile = pkgfile + + def setworkdir(self, workdir: str): + self.workdir = workdir + shutil.rmtree(workdir, ignore_errors=True) + os.mkdir(workdir) + os.mkdir(f"{workdir}/bundle") + + def write_manifest(self, manifest: dict): + with open(self.workdir + "/PkgManifest.json", "w") as f: + json.dump(manifest, f, indent=4) + + def compose(self): + shutil.make_archive(self.pkgfile, _archive_format, self.workdir) + shutil.move(self.pkgfile + _archive_suffix, self.pkgfile) + + def makedir(self, name: str): + os.mkdir(f"{self.workdir}/bundle/{name}") + + def addfile(self, src: str, dst: str): + shutil.copy2(src, f"{self.workdir}/bundle/{dst}", follow_symlinks=False) + + def add_dir(self, src: str, dst: str, merge: bool = False): + shutil.copytree(src, f"{self.workdir}/bundle/{dst}", dirs_exist_ok=merge) diff --git a/lib/sources.py b/lib/sources.py new file mode 100644 index 0000000..f6e447b --- /dev/null +++ b/lib/sources.py @@ -0,0 +1,111 @@ +import os +import sys +import tarfile +import urllib.parse +import urllib.request + + +def filename(url): + parsed = urllib.parse.urlparse(url) + path = parsed.path + filename = os.path.basename(path) + + if not filename: + raise ValueError("No filename found in URL") + + return filename + + +def download(url, output): + last_percent = -1 + + def report_progress(percent): + nonlocal last_percent + if percent != last_percent: + if last_percent >= 0: + sys.stdout.write("\b" * len(str(last_percent))) + sys.stdout.write(str(percent)) + sys.stdout.flush() + last_percent = percent + + try: + req = urllib.request.Request(url, method="GET") + req.add_header( + "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + ) + + with urllib.request.urlopen(req) as response: + total_size = response.length + if total_size is None or total_size <= 0: + print("Downloading...") + with open(output, "wb") as f: + while True: + chunk = response.read(8192) + if not chunk: + break + f.write(chunk) + print("Download completed: " + output) + return output + + sys.stdout.write("downloading... 0%") + sys.stdout.flush() + + with open(output, "wb") as f: + downloaded = 0 + block_size = 8192 + + while True: + chunk = response.read(block_size) + if not chunk: + break + f.write(chunk) + downloaded += len(chunk) + percent = int(downloaded * 100 / total_size) + report_progress(percent) + + if last_percent != 100: + report_progress(100) + + sys.stdout.write("\n") + + print("Download completed: " + output) + return output + + except Exception as e: + if os.path.exists(output): + os.remove(output) + raise Exception("Download failed: " + str(e)) + + +def tar_common_root(tar: tarfile.TarFile) -> str | None: + root = None + for member in tar.getmembers(): + this_root = member.path.split("/")[0] + if root is None: + root = this_root + if this_root != root: + return None + return root + + +def sources_dir_name() -> str: + with open("./target/sources.tag") as file: + return file.read() + + +def source_url(url: str): + if os.path.exists("./target/sources.tag"): + return + download_path = f"./target/{filename(url)}" + download(url, download_path) + + with tarfile.open(download_path, "r") as tar: + common_root = tar_common_root(tar) + extract_dir = "./target" + sources_dir = f"./target/{common_root}" + if common_root in ["", ".", "None"]: + extract_dir = "./target/sources" + sources_dir = "./target/sources" + tar.extractall(extract_dir) + with open("./target/sources.tag", "wt+") as file: + file.write(sources_dir) diff --git a/packages/semi-libc/pkgbuild.py b/packages/semi-libc/pkgbuild.py new file mode 100644 index 0000000..321507a --- /dev/null +++ b/packages/semi-libc/pkgbuild.py @@ -0,0 +1,21 @@ +from lib.cpp import * +from lib.make import * + +source_url("https://gitea.semilabs.org/semios/semi-libc/archive/v1.2.5.tar.gz") +version("1.2.5") + + +def build(): + configure(["--prefix=/"]) + make() + make_install() + + +on_build(build) + + +package("semi-libc") +use_auto_pack(["lib"]) + +package("semi-libc-dev") +use_auto_pack(["dev"]) diff --git a/x b/x new file mode 100755 index 0000000..30f39f1 --- /dev/null +++ b/x @@ -0,0 +1,4 @@ +#!/bin/sh + +cd $(dirname $0) +python3 x.py $@ diff --git a/x.py b/x.py new file mode 100644 index 0000000..1013ade --- /dev/null +++ b/x.py @@ -0,0 +1,102 @@ +import argparse +import importlib +import os +import shutil +import sys + +import hooks +import lib.arch +import lib.make +import lib.pkgcomposer +import lib.sources + +os.chdir(os.path.dirname(__file__)) +sys.path.append(os.path.abspath("lib")) + +argparser = argparse.ArgumentParser( + description="SemiOS source package repository helper" +) +argparser.add_argument( + "--target", type=str, help="Target architecture to build the package for" +) +argparser.add_argument("--build", type=str, help="Package to build") +argparser.add_argument("--clean", type=str, help="Package to clean") +args = argparser.parse_args() + +if args.target: + lib.arch.set_target(args.target) +else: + lib.arch.set_target(lib.arch.get_target()) + + +def run_build_package(package: str): + try: + os.chdir(f"packages/{package}") + os.makedirs("target", exist_ok=True) + except Exception: + print(f"No such package {package}") + exit(1) + + package_dir = os.getcwd() + + importlib.import_module(f"packages.{package}.pkgbuild") + + # Check architecture support + if not lib.make._supported_arch(lib.arch.get_target()): + print(f"Architecture {lib.arch.get_target()} is not supported for this package") + exit(1) + + # Build the package + if lib.make._on_build: + os.chdir(lib.sources.sources_dir_name()) + lib.make._on_build() + else: + print("on_build(): Not defined") + + # Run `post_build` hooks + for n in hooks.post_build.keys(): + if n in lib.make._blocked_hooks: + continue + hooks.post_build[n]() + + # Pack packages + for pkgname in lib.make._pkginfo.keys(): + if pkgname == lib.make._key_global: + continue + + pkginfo = lib.make._pkginfo[pkgname] + pkgver = pkginfo[lib.make._key_version] + if pkginfo.get(lib.make._key_arch_any): + pkgarch = "any" + else: + pkgarch = lib.arch.get_target() + + pkgfilename = f"{pkgname}={pkgver}@{pkgarch}" + + composer = lib.pkgcomposer.PkgComposer( + f"{package_dir}/target/{pkgfilename}.pkg" + ) + composer.setworkdir(f"{package_dir}/target/{pkgfilename}") + + pkginfo[lib.make._key_on_pack](composer) + + manifest = { + "name": pkgname, + "version": pkgver, + "arch": pkgarch, + } + composer.write_manifest(manifest) + composer.compose() + + +def run_clean_package(package: str): + try: + shutil.rmtree(f"packages/{package}/target") + except Exception: + pass + + +if args.build: + run_build_package(args.build) +if args.clean: + run_clean_package(args.clean)