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()