arch-image-builder/builder/build/filesystem.py
BigfootACA e6c6ab6bf7 Initial commit
Signed-off-by: BigfootACA <bigfoot@classfun.cn>
2024-05-17 23:04:34 +08:00

143 lines
4.4 KiB
Python

import os
import shutil
from logging import getLogger
from builder.lib import utils
from builder.component import user
from builder.lib.config import ArchBuilderConfigError
from builder.lib.context import ArchBuilderContext
log = getLogger(__name__)
def chroot_run(
ctx: ArchBuilderContext,
cmd: str | list[str],
cwd: str = None,
env: dict = None,
stdin: str | bytes = None,
) -> int:
"""
Chroot into rootfs and run programs
If you are running a cross build, you need install qemu-user-static-binfmt
"""
if not ctx.chroot:
raise RuntimeError("rootfs is not ready for chroot")
path = ctx.get_rootfs()
args = ["chroot", path]
args.extend(utils.parse_cmd_args(cmd))
return ctx.run_external(args, cwd, env, stdin)
def proc_mkdir(ctx: ArchBuilderContext, file: dict, path: str):
root = ctx.get_rootfs()
dir_uid, dir_gid, dir_mode = 0, 0, 0o0755
if "mkdir" in file:
if type(file["mkdir"]) is bool:
# mkdir = False: skip mkdir
if not file["mkdir"]: return
elif type(file["mkdir"]) is dict:
if "mode" in file: dir_mode = int(file["mode"])
dir_uid, dir_gid = user.parse_user_from(ctx, file)
# mkdir recursive
def mkdir_loop(folder: str):
# strip end slash
if folder.endswith("/"): folder = folder[0:-1]
if len(folder) == 0: return
# resolve to rootfs
real = os.path.join(root, folder)
if os.path.exists(real): return
# create parent folder first
mkdir_loop(os.path.dirname(folder))
log.debug(f"create folder {real} with {dir_mode:04o}")
os.mkdir(real, mode=dir_mode)
log.debug(f"chown folder {real} to {dir_uid}:{dir_gid}")
os.chown(real, uid=dir_uid, gid=dir_gid)
mkdir_loop(os.path.dirname(path))
def check_allowed(path: str, action: str):
"""
Check add / remove files is in allowed files
Why we cannot write into others folder?
1. Write to pacman managed folders (/usr, /opt, ...) WILL BREAK SYSTEM UPGRADE
2. Never add files to homes (/home/xxx, /root, ...),
when someone create new users, these configs will broken.
What if I want to write to other folders?
1. /usr/bin/ /usr/lib/ /opt/ ...: you should not add files here,
please make a package and install via pacman.
2. /home/xxx: add files into /etc/skel, they will copy when user create
3. /usr/lib/systemd/system: do not add service or override into here,
please use /etc/systemd/system (see Unit File Load Path in man:systemd.unit(5))
4. /run /tmp /dev: there are mount as virtual filesystem when booting,
you can use systemd-tmpfiles to create in these folders (/etc/tmpfiles.d)
Why these folder is writable
1. /etc/ is used for administrator configs
2. /boot/ is used for system boot up, you can put bootloaders configs into this folder
"""
if not path.startswith(("/etc/", "/boot/")):
raise ArchBuilderConfigError(f"{action} {path} is not allowed")
def add_file(ctx: ArchBuilderContext, file: dict):
# at least path content
if "path" not in file:
raise ArchBuilderConfigError("no path set in file")
if "content" not in file:
raise ArchBuilderConfigError("no content set in file")
root = ctx.get_rootfs()
path: str = file["path"]
if path.startswith("/"): path = path[1:]
uid, gid = user.parse_user_from(ctx, file)
# file encoding. default to UTF-8
encode = file["encode"] if "encode" in file else "utf-8"
# follow symbolic links
follow = file["follow"] if "follow" in file else True
# files mode
mode = int(file["mode"]) if "mode" in file else 0o0644
check_allowed(file["path"], "add files into")
# create parent folders
proc_mkdir(ctx, file, path)
# resolve to rootfs
real = os.path.join(root, path)
if not follow and os.path.exists(real): os.remove(real)
log.debug(f"create file {real}")
with open(real, "wb") as f:
content: str = file["content"]
log.debug(
"write to %s with %s",
(real, content.strip())
)
f.write(content.encode(encode))
log.debug(f"chmod file {real} to {mode:04o}")
os.chmod(real, mode=mode)
log.debug(f"chown file {real} to {uid}:{gid}")
os.chown(real, uid=uid, gid=gid)
log.info("adding file %s successful", file["path"])
def add_files_all(ctx: ArchBuilderContext, stage: str = None):
for file in ctx.get("filesystem.files", []):
cs = file["stage"] if "stage" in file else None
if cs != stage: continue
add_file(ctx, file)
def remove_all(ctx: ArchBuilderContext):
for file in ctx.get("filesystem.remove", []):
check_allowed(file, "remove files from")
shutil.rmtree(file)
def proc_filesystem(ctx: ArchBuilderContext):
add_files_all(ctx)
remove_all(ctx)