baiji 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python
  2. import debian.changelog
  3. import debian.deb822
  4. import subprocess
  5. import textwrap
  6. import argparse
  7. import tempfile
  8. import tarfile
  9. import hashlib
  10. import shutil
  11. import sys
  12. import os
  13. # Python decorator voodoo to simplify argparse subparser setup.
  14. def arg(*a, **k):
  15. return a, k
  16. def cmd(*args):
  17. def wrapper(func):
  18. def setup(subp):
  19. for a, k in args:
  20. subp.add_argument(*a, **k)
  21. subp.set_defaults(func = func)
  22. func._setup_parser = setup
  23. return func
  24. return wrapper
  25. # Context manager for temporary directories.
  26. class tempdir(object):
  27. def __enter__(self):
  28. self.dn = tempfile.mkdtemp()
  29. return self.dn
  30. def __exit__(self, *oops):
  31. shutil.rmtree(self.dn)
  32. # Docker process, mostly a context manager around subprocess.Popen.
  33. # We could use the native Python Docker interface, but the packaged
  34. # Debian version of that has a wildly different API than the version
  35. # on GitHub.
  36. class Docker(subprocess.Popen):
  37. class DockerError(Exception):
  38. "Docker returned failure."
  39. def __init__(self, *args, **kwargs):
  40. super(Docker, self).__init__(("docker",) + args, **kwargs)
  41. def __enter__(self):
  42. return self
  43. def __exit__(self, *oops):
  44. if any(oops):
  45. return
  46. if self.stdin:
  47. self.stdin.close()
  48. status = self.wait()
  49. if status:
  50. raise self.DockerError()
  51. # Filter which acts like fakeroot for tarfile.TarFile.add()
  52. def fakeroot_filter(info):
  53. info.uname = info.gname = "root"
  54. info.uid = info.gid = 0
  55. return info
  56. # Commands
  57. @cmd(arg("--dist", default = "jessie",
  58. help = "distribution for base docker image"),
  59. arg("--tag", default = "baiji:jessie",
  60. help = "tag to use for constructed base docker image"),
  61. )
  62. def create(args):
  63. """
  64. Construct a base Docker image.
  65. This is mostly just the output of debootstrap, with a bit of extra
  66. setup to include git, build-essentials, and fakeroot.
  67. """
  68. with tempdir() as dn:
  69. subprocess.check_call(("fakeroot", "/usr/sbin/debootstrap",
  70. "--foreign", "--variant=buildd", args.dist, dn))
  71. with Docker("import", "-", args.tag, stdin = subprocess.PIPE) as docker:
  72. with tarfile.open(mode = "w|", fileobj = docker.stdin) as tar:
  73. tar.add(dn, ".", filter = fakeroot_filter)
  74. with Docker("build", "-t", args.tag, "-", stdin = subprocess.PIPE) as docker:
  75. docker.communicate(textwrap.dedent('''\
  76. FROM {args.tag}
  77. RUN sed -i '/mount -t proc /d; /mount -t sysfs /d' /debootstrap/functions && \\
  78. /debootstrap/debootstrap --second-stage
  79. RUN apt-get update && \\
  80. apt-get install -y --no-install-recommends build-essential fakeroot git apt-utils
  81. RUN useradd -U -m -d /build baiji
  82. WORKDIR /build
  83. '''.format(args = args)))
  84. @cmd(arg("--tag", default = "baiji:jessie",
  85. help = "tag of base docker image to update"),
  86. )
  87. def update(args):
  88. """
  89. Update a base Docker image.
  90. """
  91. with Docker("build", "-t", args.tag, "-", stdin = subprocess.PIPE) as docker:
  92. docker.communicate(textwrap.dedent('''\
  93. FROM {args.tag}
  94. RUN apt-get update && \\
  95. apt-get upgrade -y --with-new-pkgs --no-install-recommends && \\
  96. apt-get autoremove && \\
  97. apt-get clean
  98. '''.format(args = args)))
  99. @cmd(arg("--tag", default = "baiji:jessie",
  100. help = "tag of base docker image to use"),
  101. arg("--dsc", type = argparse.FileType("r"),
  102. help = ".dsc file to build"),
  103. arg("--local-package", default = [], nargs = "+",
  104. help = "local packages to make available to build"),
  105. arg("--force-image", action = "store_true",
  106. help = "don't rebuild Docker image"),
  107. arg("--dont-clean", action = "store_true",
  108. help = "don't clean up old Docker images"),
  109. arg("--just-image", action = "store_true",
  110. help = "don't build, just generate Docker image"),
  111. )
  112. def build(args):
  113. """
  114. Build a binary package given a source package.
  115. If no source package supplied, try to build one from the current
  116. directory, like debuild.
  117. """
  118. if args.dsc is None:
  119. try:
  120. subprocess.check_call(("dpkg-buildpackage", "-S", "-us", "-uc", "-rfakeroot"))
  121. except Exception as e:
  122. sys.exit("Couldn't build source package: {!s}".format(e))
  123. try:
  124. with open("debian/changelog") as f:
  125. changelog = debian.changelog.Changelog(f)
  126. args.dsc = open("../{}_{}{}.dsc".format(
  127. changelog.package, changelog.upstream_version,
  128. "" if changelog.debian_revision is None else "-" + changelog.debian_revision))
  129. except Exception as e:
  130. sys.exit("Couldn't find .dsc file: {!s}".format(e))
  131. dsc = debian.deb822.Dsc(args.dsc)
  132. dummy = debian.deb822.Deb822()
  133. dummy_name = "baiji-depends-" + dsc["Source"]
  134. dummy_fn = "{}_{}_all.deb".format(dummy_name, dsc["Version"])
  135. dummy["Depends"] = ", ".join(dsc[i]
  136. for i in ("Build-Depends",
  137. "Build-Depends-Indep",
  138. "Build-Depends-Arch")
  139. if i in dsc)
  140. dummy["Package"] = dummy_name
  141. for tag in ("Version", "Maintainer", "Homepage"):
  142. dummy[tag] = dsc[tag]
  143. build_image_name = "baiji/build/{}/{}".format(dsc["Source"], dsc["Version"])
  144. build_image_hash = hashlib.sha1(dummy["Depends"]).hexdigest()
  145. build_image = "{}:{}".format(build_image_name, build_image_hash)
  146. with Docker("image", "ls", build_image_name, "--format", "{{.Tag}}",
  147. stdout = subprocess.PIPE) as docker:
  148. build_image_hashes = set(docker.stdout.read().split())
  149. build_image_exists = build_image_hash in build_image_hashes
  150. make_build_image = args.force_image or not build_image_exists
  151. if args.dont_clean:
  152. build_image_hashes = { build_image_hash } if build_image_exists else set()
  153. if not make_build_image:
  154. build_image_hashes.discard(build_image_hash)
  155. for h in build_image_hashes:
  156. with Docker("rmi", "{}:{}".format(build_image_name, h)):
  157. pass
  158. if make_build_image:
  159. with tempdir() as dn:
  160. equivs = subprocess.Popen(("equivs-build", "/dev/stdin"),
  161. stdin = subprocess.PIPE, stdout = subprocess.PIPE, cwd = dn)
  162. equivs.communicate(str(dummy))
  163. if equivs.wait():
  164. sys.exit("Couldn't generate dummy dependency package")
  165. with open(os.path.join(dn, "Dockerfile"), "w") as f:
  166. f.write(textwrap.dedent('''\
  167. FROM {args.tag}
  168. COPY build.sh /baiji/
  169. COPY micro-apt /micro-apt/
  170. RUN cd /micro-apt && \\
  171. apt-ftparchive packages . > Packages
  172. RUN cd /etc/apt/sources.list.d && \\
  173. echo 'deb [trusted=yes] file:///micro-apt ./' > micro-apt.list
  174. RUN apt-get update && \\
  175. apt-get install -y --no-install-recommends {dummy_name} && \\
  176. apt-get clean
  177. USER baiji
  178. '''.format(args = args, dummy_name = dummy_name)))
  179. with open(os.path.join(dn, "build.sh"), "w") as f:
  180. f.write(textwrap.dedent('''\
  181. #!/bin/bash -
  182. set -eo pipefail
  183. arch=`dpkg-architecture -qDEB_BUILD_ARCH`
  184. dpkg-source -x /source/{source}_{version}.dsc
  185. cd {source}-{version}
  186. dpkg-buildpackage -b -uc -us 2>&1 | tee ../{source}_{version}_$arch.build
  187. cd ..
  188. rm -rf {source}-{version}
  189. '''.format(source = dsc["Source"], version = dsc["Version"])))
  190. with Docker("build", "-t", build_image, "-", stdin = subprocess.PIPE) as docker:
  191. with tarfile.open(mode = "w|", fileobj = docker.stdin) as tar:
  192. for fn in ("Dockerfile", "build.sh"):
  193. tar.add(os.path.join(dn, fn), fn, filter = fakeroot_filter)
  194. for pkg in [os.path.join(dn, dummy_fn)] + args.local_package:
  195. tar.add(pkg, os.path.join("micro-apt", os.path.basename(pkg)),
  196. filter = fakeroot_filter)
  197. if not args.just_image:
  198. container_name = "baiji-build-{}".format(dsc["Source"])
  199. dn = os.path.dirname(args.dsc.name)
  200. with Docker("run", "-i", "--name", container_name, "--network", "none",
  201. "-v", "{}:/source:ro".format(os.path.abspath(dn)),
  202. build_image, "/bin/bash", "-x", "/baiji/build.sh"):
  203. pass
  204. with Docker("cp", "{}:/build/.".format(container_name), "-",
  205. stdout = subprocess.PIPE) as docker:
  206. with tarfile.open(mode = "r|*", fileobj = docker.stdout) as tar:
  207. for member in tar:
  208. fn = os.path.basename(member.name)
  209. if any(fn.endswith(fn2) for fn2 in (".deb", ".changes")):
  210. with open(os.path.join(dn, fn), "w") as f:
  211. f.write(tar.extractfile(member).read())
  212. with Docker("rm", container_name):
  213. pass
  214. # Parse arguments and dispatch to one of the commands above.
  215. def main():
  216. HF = type("HF", (argparse.ArgumentDefaultsHelpFormatter,
  217. argparse.RawDescriptionHelpFormatter), {})
  218. parser = argparse.ArgumentParser(formatter_class = HF, description = __doc__)
  219. subparsers = parser.add_subparsers(title = "Commands", metavar = "")
  220. for name in sorted(globals()):
  221. func = globals()[name]
  222. try:
  223. setup_parser = func._setup_parser
  224. except:
  225. continue
  226. setup_parser(subparsers.add_parser(name.replace("_", "-"),
  227. formatter_class = HF,
  228. description = func.__doc__,
  229. help = (func.__doc__ or "").lstrip().partition("\n")[0]))
  230. args = parser.parse_args()
  231. args.func(args)
  232. if __name__ == "__main__":
  233. main()