taler-deployment

Deployment scripts and configuration files
Log | Files | Refs | README

generic (8213B)


      1 #!/usr/bin/env python3
      2 # This file is in the public domain.
      3 # Helper script to build the latest DEB packages in the container.
      4 
      5 # Supported tag syntax variants:
      6 # v$maj.$min$.$patch => release version
      7 # v$maj.$min$.$patch-dev.$n => dev version
      8 # deb-v$maj.$min$.$patch-$revision => release version with debian revision
      9 # Debian revisions of dev versions are *not* supported
     10 
     11 import os
     12 import sys
     13 import subprocess
     14 import glob
     15 import shutil
     16 import shlex
     17 from email.utils import formatdate
     18 
     19 def run_cmd(cmd, shell=False, cwd=None, env=None):
     20     """Helper to run commands and exit on failure (mimicking set -e)."""
     21     # If specific env vars are passed, update the current env, otherwise use current
     22     command_env = os.environ.copy()
     23     if env:
     24         command_env.update(env)
     25     
     26     # Flush stdout so logs appear in order
     27     sys.stdout.flush()
     28     
     29     subprocess.check_call(cmd, shell=shell, cwd=cwd, env=command_env)
     30 
     31 def get_output(cmd, shell=False, cwd=None):
     32     """Helper to get command output as string."""
     33     return subprocess.check_output(cmd, shell=shell, cwd=cwd, text=True).strip()
     34 
     35 def get_tag_debver(tag):
     36     """Get a debian version string from a git tag"""
     37     if tag.startswith("v"):
     38         devsuff = "-dev."
     39         d = tag.find(devsuff)
     40         if d < 0:
     41             return tag[1:]
     42         else:
     43             return tag[1:d] + "~dev" + tag[d + len(devsuff)]
     44     if tag.startswith("deb-v"):
     45         tag = tag[5:]
     46         if "-" in tag:
     47             a, b = tag.split("-")
     48             return a + ":" + b
     49         return tag
     50     raise Error("unexpected tag format")
     51 
     52 
     53 
     54 def make_codename_version(deb_version, build_codename):
     55     if "-" in deb_version:
     56         # We already have a dash, thus a debian version.
     57         # Append out build_codename to that.
     58         return f"{deb_version}+{build_codename}"
     59     # We don't have a debian version yet, start at zero, because by convention,
     60     # real / manually created debian release versions start at 1.
     61     return f"{deb_version}-0+{build_codename}"
     62 
     63 
     64 def main():
     65     if 'LD_LIBRARY_PATH' in os.environ:
     66         del os.environ['LD_LIBRARY_PATH']
     67 
     68     # Arguments
     69     if len(sys.argv) < 3:
     70         print("Usage: script.py <PACKAGE> <CODENAME> <ARCH>", file=sys.stderr)
     71         sys.exit(1)
     72 
     73     PACKAGE = sys.argv[1]
     74     CODENAME = sys.argv[2]
     75     ARCH = sys.argv[3]
     76 
     77     # Path of the debian/ folder in the repository
     78     DEBIANPATH = ""
     79     debpath_file = f"/buildconfig/{PACKAGE}.debpath"
     80     if os.path.exists(debpath_file):
     81         with open(debpath_file, 'r') as f:
     82             DEBIANPATH = f.read().strip()
     83 
     84     print(f"Building {PACKAGE} with generic build logic", file=sys.stderr)
     85 
     86     with open(f"/buildconfig/{PACKAGE}.tag", 'r') as f:
     87         TAG = f.read().strip()
     88 
     89     # 1. Setup Local Repo
     90     os.chdir("/pkgdir")
     91     # Using shell=True here to easily handle the pipe logic
     92     run_cmd("dpkg-scanpackages . | xz - > /pkgdir/Packages.xz", shell=True)
     93     
     94     with open("/etc/apt/sources.list.d/taler-packaging-local.list", "w") as f:
     95         f.write("deb [trusted=yes] file:/pkgdir ./\n")
     96     
     97     run_cmd(["apt-get", "update"])
     98 
     99     # 2. Prepare Build Directory
    100     if not os.path.exists("/build"):
    101         os.makedirs("/build")
    102     os.chdir("/build")
    103 
    104     with open(f"/buildconfig/{PACKAGE}.giturl", 'r') as f:
    105         GITURL = f.read().strip()
    106 
    107     run_cmd(["git", "config", "--global", "advice.detachedHead", "false"])
    108     run_cmd(["git", "clone", "--depth=1", f"--branch={TAG}", GITURL, PACKAGE])
    109 
    110     build_pkg_path = os.path.join("/build", PACKAGE, DEBIANPATH)
    111     os.chdir(build_pkg_path)
    112 
    113     DEB_VERSION = get_tag_debver(TAG)
    114 
    115     # 3. Bootstrap and Install Deps
    116     os.chdir(os.path.join("/build", PACKAGE))
    117     run_cmd(["./bootstrap"])
    118 
    119     os.chdir(build_pkg_path)
    120 
    121     # Install build-time dependencies
    122     # Note: Passing the tool command as a single string because it contains spaces/options
    123     tool_cmd = "apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes"
    124     run_cmd(["mk-build-deps", "--install", f"--tool={tool_cmd}", "debian/control"])
    125 
    126     # Sparse checkout hint
    127     with open(".version", "w") as f:
    128         f.write(f"{DEB_VERSION}\n")
    129 
    130     # 4. Configure Environment for Build
    131     deb_dbg_repo = "debian/.debhelper/"
    132     os.environ["DEB_DBG_SYMBOLS_REPO"] = deb_dbg_repo
    133     os.makedirs(deb_dbg_repo, exist_ok=True)
    134     
    135     # touch command
    136     open(os.path.join(deb_dbg_repo, "debian-symbols-pool"), 'a').close()
    137     
    138     os.environ["DEB_BUILD_MAINT_OPTIONS"] = "debug"
    139 
    140     # Add a codename-specific tag to the package.
    141     # We need this tag as reprepro doesn't support multiple packages (even for
    142     # different codenames like trixie/bookworm) per repo.
    143     # Examples of how it should work:
    144     # 1.2.3 => 1.2.3-0~trixie
    145     # 1.2.3-2 => 1.2.3-2~trixie
    146     # What we must not do:
    147     # WRONG: 1.2.3 => 1.2.3~trixie
    148     # Because 1.2.3~trixie <= 1.2.3, version requirements in dependencies would
    149     # not work.
    150 
    151     out_deb_version = make_codename_version(DEB_VERSION, CODENAME)
    152 
    153     print(f"Building {PACKAGE} as version {out_deb_version}", file=sys.stderr)
    154 
    155     debian_date = formatdate(localtime=True)
    156 
    157     changelog = f"""\
    158 {PACKAGE} ({out_deb_version}) unstable; urgency=low
    159 
    160   * Release {out_deb_version}.
    161 
    162  -- Taler Packaging Team <deb@taler.net>  {debian_date}
    163 """
    164 
    165     with open("debian/changelog", "w") as f:
    166         f.write(changelog)
    167 
    168     # 5. Build Package
    169     run_cmd(["dpkg-buildpackage", "-rfakeroot", "-b", "-uc", "-us"])
    170 
    171     # Copy artifacts
    172     # Globs for ../*.deb and ../*.ddeb relative to current dir
    173     deb_files = glob.glob("../*.deb")
    174     ddeb_files = glob.glob("../*.ddeb")
    175 
    176     for f in deb_files:
    177         shutil.copy(f, "/pkgdir/")
    178     
    179     for f in ddeb_files:
    180         shutil.copy(f, "/pkgdir/")
    181 
    182     # Save built filenames
    183     built_current_path = f"/pkgdir/{PACKAGE}@{ARCH}.built.current"
    184     with open(built_current_path, 'w') as f:
    185         for deb in deb_files:
    186             f.write(os.path.basename(deb) + "\n")
    187         for ddeb in ddeb_files:
    188             f.write(os.path.basename(ddeb) + "\n")
    189 
    190     # 6. Testing
    191     # Re-scan
    192     os.chdir("/pkgdir")
    193     run_cmd("dpkg-scanpackages . | xz - > /pkgdir/Packages.xz", shell=True)
    194     
    195     with open("/etc/apt/sources.list.d/taler-packaging-local.list", "w") as f:
    196         f.write("deb [trusted=yes] file:/pkgdir ./\n")
    197         
    198     run_cmd(["apt-get", "update"])
    199 
    200     print(f"Installing built packages from {build_pkg_path}/..", file=sys.stderr)
    201 
    202     # Since we are in /pkgdir, and files were copied there, we glob current dir.
    203     pkgdir_debs = glob.glob(f"{build_pkg_path}/../*.deb")
    204     if pkgdir_debs:
    205         run_cmd(["apt", "install", "-y"] + pkgdir_debs)
    206 
    207     # Check Binaries
    208     # Iterate over original source locations (copied from logic `../*.deb`)
    209     # But since we are in /pkgdir now, let's use the files we just identified.
    210     for deb in pkgdir_debs:
    211         # Get contents: dpkg --contents
    212         contents = get_output(["dpkg", "--contents", deb])
    213         
    214         for line in contents.splitlines():
    215             parts = line.split()
    216             if len(parts) < 6: 
    217                 print("failed to read package contents", file=sys.stderr)
    218                 sys.exit(3)
    219             
    220             # dpkg --contents output format looks like:
    221             # drwxr-xr-x root/root 0 2023-01-01 12:00 ./usr/bin/
    222             fname = parts[5]
    223             
    224             if "bin" not in fname:
    225                 continue
    226                 
    227             # Convert to absolute path
    228             if fname.startswith("./"):
    229                 fname = fname[2:]
    230             if not fname.startswith("/"):
    231                 fname = "/" + fname
    232 
    233             # Check if ELF executable
    234             file_info = get_output(["file", fname])
    235             if "ELF" in file_info and "executable" in file_info:
    236                 print(f"checking {fname}")
    237                 try:
    238                     run_cmd(["ldd", fname])
    239                 except subprocess.CalledProcessError:
    240                     print(f"Installed binary {fname} has linker issue")
    241                     sys.exit(2)
    242 
    243     # Finalize tag
    244     with open(f"/pkgdir/{PACKAGE}@{ARCH}.built.tag", "w") as f:
    245         f.write(TAG + "\n")
    246 
    247 if __name__ == "__main__":
    248     main()