Add Debian-specific OSBuild stages: apt, debootstrap, ostree-commit
Some checks are pending
Checks / Spelling (push) Waiting to run
Checks / Python Linters (push) Waiting to run
Checks / Shell Linters (push) Waiting to run
Checks / 📦 Packit config lint (push) Waiting to run
Checks / 🔍 Check for valid snapshot urls (push) Waiting to run
Checks / 🔍 Check JSON files for formatting consistency (push) Waiting to run
Generate / Documentation (push) Waiting to run
Generate / Test Data (push) Waiting to run
Tests / Unittest (push) Waiting to run
Tests / Assembler test (legacy) (push) Waiting to run
Tests / Smoke run: unittest as normal user on default runner (push) Waiting to run

This commit is contained in:
robojerk 2025-08-22 18:11:39 -07:00
parent 3e37ad3b92
commit 31162116f8
7 changed files with 465 additions and 105 deletions

2
.gitignore vendored
View file

@ -25,3 +25,5 @@ venv
/.tox
/test/data/certs/lib.sh
debian-forge

82
stages/org.osbuild.apt Normal file
View file

@ -0,0 +1,82 @@
#!/usr/bin/python3
import os
import sys
import subprocess
import json
import osbuild.api
def run_apt_command(tree, command, env=None):
"""Run apt command in the target filesystem"""
if env is None:
env = {}
# Set up environment for non-interactive operation
apt_env = {
"DEBIAN_FRONTEND": "noninteractive",
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
apt_env.update(env)
# Run command in chroot
cmd = ["chroot", tree] + command
result = subprocess.run(cmd, env=apt_env, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error running apt command: {command}")
print(f"stdout: {result.stdout}")
print(f"stderr: {result.stderr}")
return False
return True
def main(tree, options):
"""Main function for apt stage"""
# Get options
packages = options.get("packages", [])
recommends = options.get("recommends", False)
unauthenticated = options.get("unauthenticated", False)
update = options.get("update", True)
if not packages:
print("No packages specified for installation")
return 1
# Update package lists if requested
if update:
print("Updating package lists...")
if not run_apt_command(tree, ["apt-get", "update"]):
return 1
# Build apt-get install command
apt_options = ["apt-get", "-y"]
if not recommends:
apt_options.append("--no-install-recommends")
if unauthenticated:
apt_options.append("--allow-unauthenticated")
apt_options.extend(["install"] + packages)
# Install packages
print(f"Installing packages: {', '.join(packages)}")
if not run_apt_command(tree, apt_options):
return 1
# Clean up package cache
print("Cleaning package cache...")
if not run_apt_command(tree, ["apt-get", "clean"]):
return 1
print("Package installation completed successfully")
return 0
if __name__ == '__main__':
args = osbuild.api.arguments()
r = main(args["tree"], args["options"])
sys.exit(r)

View file

@ -0,0 +1,52 @@
{
"name": "org.osbuild.apt",
"version": "1",
"description": "Install Debian packages using apt",
"options": {
"type": "object",
"properties": {
"packages": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of packages to install"
},
"recommends": {
"type": "boolean",
"default": false,
"description": "Install recommended packages"
},
"unauthenticated": {
"type": "boolean",
"default": false,
"description": "Allow unauthenticated packages"
},
"update": {
"type": "boolean",
"default": true,
"description": "Update package lists before installation"
}
},
"required": ["packages"]
},
"inputs": {
"type": "object",
"additionalProperties": false
},
"devices": {
"type": "object",
"additionalProperties": false
},
"mounts": {
"type": "object",
"additionalProperties": false
},
"capabilities": {
"type": "array",
"items": {
"type": "string"
},
"default": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_MKNOD", "CAP_SETGID", "CAP_SETUID", "CAP_SYS_CHROOT"]
}
}

View file

@ -0,0 +1,111 @@
#!/usr/bin/python3
import os
import sys
import subprocess
import tempfile
import shutil
import osbuild.api
def run_debootstrap(suite, target, mirror, arch=None, variant=None, extra_packages=None):
"""Run debootstrap to create base Debian filesystem"""
cmd = ["debootstrap"]
if arch:
cmd.extend(["--arch", arch])
if variant:
cmd.extend(["--variant", variant])
if extra_packages:
cmd.extend(["--include", ",".join(extra_packages)])
cmd.extend([suite, target, mirror])
print(f"Running debootstrap: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error running debootstrap:")
print(f"stdout: {result.stdout}")
print(f"stderr: {result.stderr}")
return False
return True
def setup_apt_sources(tree, suite, mirror):
"""Set up apt sources.list for the target filesystem"""
sources_content = f"""deb {mirror} {suite} main
deb {mirror} {suite}-updates main
deb {mirror} {suite}-security main
"""
sources_path = os.path.join(tree, "etc/apt/sources.list")
os.makedirs(os.path.dirname(sources_path), exist_ok=True)
with open(sources_path, "w") as f:
f.write(sources_content)
print(f"Created sources.list for {suite}")
def main(tree, options):
"""Main function for debootstrap stage"""
# Get options
suite = options.get("suite", "bookworm")
mirror = options.get("mirror", "http://deb.debian.org/debian")
arch = options.get("arch")
variant = options.get("variant", "minbase")
extra_packages = options.get("extra_packages", [])
if not suite:
print("No suite specified for debootstrap")
return 1
if not mirror:
print("No mirror specified for debootstrap")
return 1
# Create temporary directory for debootstrap
with tempfile.TemporaryDirectory() as temp_dir:
print(f"Creating base Debian filesystem in {temp_dir}")
# Run debootstrap
if not run_debootstrap(suite, temp_dir, mirror, arch, variant, extra_packages):
return 1
# Set up apt sources
setup_apt_sources(temp_dir, suite, mirror)
# Copy files to target tree
print(f"Copying filesystem to target tree: {tree}")
# Ensure target directory exists
os.makedirs(tree, exist_ok=True)
# Copy all files from temp directory to target
for item in os.listdir(temp_dir):
src = os.path.join(temp_dir, item)
dst = os.path.join(tree, item)
if os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
print("Base Debian filesystem created successfully")
return 0
if __name__ == '__main__':
args = osbuild.api.arguments()
r = main(args["tree"], args["options"])
sys.exit(r)

View file

@ -0,0 +1,57 @@
{
"name": "org.osbuild.debootstrap",
"version": "1",
"description": "Create base Debian filesystem using debootstrap",
"options": {
"type": "object",
"properties": {
"suite": {
"type": "string",
"default": "bookworm",
"description": "Debian suite to bootstrap (e.g., bookworm, sid)"
},
"mirror": {
"type": "string",
"default": "http://deb.debian.org/debian",
"description": "Debian mirror URL"
},
"arch": {
"type": "string",
"description": "Target architecture (e.g., amd64, arm64)"
},
"variant": {
"type": "string",
"default": "minbase",
"description": "Debootstrap variant (e.g., minbase, buildd)"
},
"extra_packages": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Additional packages to include in base filesystem"
}
},
"required": ["suite", "mirror"]
},
"inputs": {
"type": "object",
"additionalProperties": false
},
"devices": {
"type": "object",
"additionalProperties": false
},
"mounts": {
"type": "object",
"additionalProperties": false
},
"capabilities": {
"type": "array",
"items": {
"type": "string"
},
"default": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_MKNOD", "CAP_SETGID", "CAP_SETUID"]
}
}

View file

@ -1,73 +1,122 @@
#!/usr/bin/python3
import json
import os
import subprocess
import sys
import subprocess
import json
import tempfile
from osbuild import api
from osbuild.util import ostree
import osbuild.api
def main(inputs, output_dir, options, meta):
tree = inputs["tree"]["path"]
selinux_label_version = options.get("selinux-label-version", 0)
def run_ostree_command(cmd, cwd=None, env=None):
"""Run ostree command and return result"""
if env is None:
env = {}
result = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error running ostree command: {' '.join(cmd)}")
print(f"stdout: {result.stdout}")
print(f"stderr: {result.stderr}")
return False, result.stderr
return True, result.stdout
ref = options["ref"]
os_version = options.get("os_version", None)
parent = options.get("parent", None)
# rpm-ostree compose commit wants to consume the commit
# so for now, we copy the whole tree and let it have its
# way
with tempfile.TemporaryDirectory(dir=output_dir) as root:
subprocess.run(["cp", "--reflink=auto", "-a",
f"{tree}/.", root],
check=True)
def create_ostree_repo(repo_path):
"""Create OSTree repository if it doesn't exist"""
if not os.path.exists(repo_path):
print(f"Creating OSTree repository at {repo_path}")
success, _ = run_ostree_command(["ostree", "init", "--repo", repo_path])
if not success:
return False
return True
repo = os.path.join(output_dir, "repo")
treefile = ostree.Treefile()
treefile["ref"] = ref
if selinux_label_version != 0:
# Don't set if 0 (default), to support older rpm-ostree versions
treefile["selinux-label-version"] = selinux_label_version
def create_commit(repo_path, tree_path, branch, subject, metadata=None, collection_id=None, ref_binding=None):
"""Create OSTree commit from filesystem tree"""
# Prepare commit command
cmd = ["ostree", "commit", "--repo", repo_path, "--branch", branch]
if subject:
cmd.extend(["--subject", subject])
if collection_id:
cmd.extend(["--collection-id", collection_id])
if ref_binding:
for ref in ref_binding:
cmd.extend(["--ref-binding", ref])
if metadata:
for key, value in metadata.items():
cmd.extend(["--add-metadata-string", f"{key}={value}"])
# Add tree path
cmd.append(tree_path)
print(f"Creating OSTree commit: {' '.join(cmd)}")
success, output = run_ostree_command(cmd)
if not success:
return False, None
# Extract commit hash from output
commit_hash = output.strip()
print(f"Created commit: {commit_hash}")
return True, commit_hash
argv = ["rpm-ostree", "compose", "commit"]
argv += [f"--repo={repo}"]
if parent:
argv += [f"--parent={parent}"]
if os_version:
argv += [
f"--add-metadata-string=version={os_version}",
]
argv += [
f"--add-metadata-string=rpmostree.inputhash={meta['id']}",
f"--write-composejson-to={output_dir}/compose.json"
]
with treefile.as_tmp_file() as path:
argv += [path, root]
subprocess.run(argv,
stdout=sys.stderr,
check=True)
with open(os.path.join(output_dir, "compose.json"), "r", encoding="utf8") as f:
compose = json.load(f)
api.metadata({"compose": compose})
def main(tree, options):
"""Main function for ostree commit stage"""
# Get options
repository = options.get("repository", "ostree-repo")
branch = options.get("branch", "debian/atomic")
subject = options.get("subject", "Debian atomic commit")
metadata = options.get("metadata", {})
collection_id = options.get("collection_id")
ref_binding = options.get("ref_binding", [])
if not branch:
print("No branch specified for OSTree commit")
return 1
# Create repository path
repo_path = os.path.join(tree, repository)
# Create OSTree repository
if not create_ostree_repo(repo_path):
return 1
# Create commit
success, commit_hash = create_commit(
repo_path, tree, branch, subject, metadata, collection_id, ref_binding
)
if not success:
return 1
# Write commit info to output
commit_info = {
"repository": repository,
"branch": branch,
"commit": commit_hash,
"subject": subject
}
output_file = os.path.join(tree, "ostree-commit.json")
with open(output_file, "w") as f:
json.dump(commit_info, f, indent=2)
print("OSTree commit created successfully")
return 0
if __name__ == '__main__':
args = api.arguments()
r = main(args["inputs"],
args["tree"],
args["options"],
args["meta"])
args = osbuild.api.arguments()
r = main(args["tree"], args["options"])
sys.exit(r)

View file

@ -1,58 +1,65 @@
{
"summary": "Assemble a file system tree into a ostree commit",
"description": [
"Needs a file system tree that is already conforming to the ostree",
"system layout[1], specified via the `tree` input and commits it",
"to a repository. The repository must have been created at `/repo`.",
"Additional metadata is stored in `/compose.json` which contains",
"the commit compose information. This is also returned via the",
"metadata API to osbuild.",
"[1] https://ostree.readthedocs.io/en/stable/manual/adapting-existing/"
],
"capabilities": [
"CAP_MAC_ADMIN",
"CAP_NET_ADMIN",
"CAP_SYS_PTRACE"
],
"schema_2": {
"options": {
"additionalProperties": false,
"required": [
"ref"
],
"properties": {
"ref": {
"description": "OStree ref to create for the commit",
"type": "string",
"default": ""
},
"os_version": {
"description": "Set the version of the OS as commit metadata",
"name": "org.osbuild.ostree.commit",
"version": "1",
"description": "Create OSTree commit from filesystem tree",
"options": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"default": "ostree-repo",
"description": "OSTree repository name/path"
},
"branch": {
"type": "string",
"default": "debian/atomic",
"description": "OSTree branch name for the commit"
},
"subject": {
"type": "string",
"default": "Debian atomic commit",
"description": "Commit message/subject"
},
"metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"parent": {
"description": "commit id of the parent commit",
"default": {},
"description": "Additional metadata key-value pairs"
},
"collection_id": {
"type": "string",
"description": "Collection ID for ref binding"
},
"ref_binding": {
"type": "array",
"items": {
"type": "string"
},
"selinux-label-version": {
"description": "Set selinux label version",
"type": "integer",
"default": 0
}
"default": [],
"description": "List of ref bindings for the commit"
}
},
"inputs": {
"type": "object",
"additionalProperties": false,
"required": [
"tree"
],
"properties": {
"tree": {
"type": "object",
"additionalProperties": true
}
}
}
"required": ["branch"]
},
"inputs": {
"type": "object",
"additionalProperties": false
},
"devices": {
"type": "object",
"additionalProperties": false
},
"mounts": {
"type": "object",
"additionalProperties": false
},
"capabilities": {
"type": "array",
"items": {
"type": "string"
},
"default": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_MKNOD", "CAP_SETGID", "CAP_SETUID"]
}
}