hyperboria/rules/packaging/packaging.bzl
the-superpirate 8472f27ec5 No description
GitOrigin-RevId: ddf02e70d2827c048db49b687ebbcdcc67807ca6
2021-01-04 18:12:22 +03:00

557 lines
19 KiB
Python

"""Module contains functions for package creation, publishing and deployment
Example:
You could easily create target for debian package. It will make available :deb and :publish targets
debian_package(
name = "debian-repo",
config_srcs = ["debian/debian-repo.aptly"],
depends = [
"nginx",
"aptly",
],
description = "Debian Repository Package",
extra_postinst = [
"aptly -config='/etc/debian-repo/debian-repo.aptly' repo create bionic -distribution=bionic -component=main || /bin/true",
"aptly -config='/etc/debian-repo/debian-repo.aptly' publish repo bionic || /bin/true",
],
package = "debian-repo",
package_dir = "/var/cache/aptly",
version = "1.0.0",
)
Then, you can
bazel build :deb
bazel run :publish
"""
load("@rules_pkg//:pkg.bzl", "pkg_tar")
tar_filetype = [".tar", ".tar.gz", ".tgz", ".tar.xz", ".tar.bz2"]
deb_filetype = [".deb", ".udeb"]
default_maintainer = "developers@example.com"
strip_prefix_by_action = {
None: None,
"repository": "/",
"local": "./",
}
ScriptsInfo = provider(fields = ["postinst", "postrm", "preinst", "prerm"])
def deploy_command(deb_package, hostname, username = "deployer"):
"""Command for start deployment on `target_hosts` of package.
Args:
deb_package (File): Debian package to install
hostname (str): host for installation
username (str): SSH user for installation process
Returns:
Prepared string containing full bash command to start a deployment
"""
echo = 'echo "Deploying to {hostname}"\n'
remove_old = "ssh {username}@{hostname} 'sudo rm -f {package_name}'\n"
copy = "scp {package_path} {username}@{hostname}:~\n"
remote_install = "sudo -H apt install --reinstall ./{package_name}".format(package_name = deb_package.basename)
notify = "ssh {username}@{hostname} '{remote_notify}'\n"
install = "ssh {username}@{hostname} '{remote_install}'\n"
return (echo + remove_old + copy + install + remove_old).format(
package_name = deb_package.basename,
package_path = deb_package.short_path,
hostname = hostname,
username = username,
remote_install = remote_install,
)
def _deploy(ctx):
package = None
for package in ctx.files.package:
if package.basename.endswith(".deb") and package.basename != "deb.deb":
break
if not package:
fail("No suitable debian package found!")
content = "#!/usr/bin/env bash\nset -e\n"
for target_host in ctx.attr.target_hosts:
cmd = deploy_command(package, target_host)
content += cmd
ctx.actions.write(
output = ctx.outputs.executable,
content = content,
is_executable = True,
)
runfiles = ctx.runfiles(
files = [package],
collect_default = True,
)
return [DefaultInfo(runfiles = runfiles)]
deploy = rule(
implementation = _deploy,
attrs = {
"package": attr.label(allow_files = True),
"target_hosts": attr.string_list(mandatory = True),
},
executable = True,
)
def _generate_scripts_impl(ctx):
generator = {
"postinst": {"template": ctx.file._postinst_template, "output": ctx.outputs.postinst},
"postrm": {"template": ctx.file._postrm_template, "output": ctx.outputs.postrm},
"preinst": {"template": ctx.file._preinst_template, "output": ctx.outputs.preinst},
"prerm": {"template": ctx.file._prerm_template, "output": ctx.outputs.prerm},
}
overriden_basenames = {x.basename: x for x in ctx.files.overriden_scripts}
for script in generator:
if script in overriden_basenames:
ctx.actions.run_shell(
command = "cp %s %s" % (overriden_basenames[script].path, generator[script]["output"].path),
inputs = [overriden_basenames[script]],
outputs = [generator[script]["output"]],
)
else:
args = [generator[script]["template"].path, generator[script]["output"].path]
args.extend(["--name", ctx.attr.package_name])
if ctx.attr.extra_postinst:
args.extend(["--extra-postinst", "[" + ", ".join([escape(x) for x in ctx.attr.extra_postinst]) + ", ]"])
if ctx.attr.services:
args.extend(["--services", "[" + ", ".join([escape(x) for x in ctx.attr.services]) + ", ]"])
if ctx.attr.add_user:
args.extend(["--add-user", ctx.attr.add_user])
if ctx.attr.user_groups:
args.extend(["--user-groups", "[" + ", ".join([escape(x) for x in ctx.attr.user_groups]) + ", ]"])
ctx.actions.run(
executable = ctx.executable.jinja2,
arguments = args,
inputs = [generator[script]["template"]],
outputs = [generator[script]["output"]],
mnemonic = "jinja2",
)
return [ScriptsInfo(
postinst = ctx.outputs.postinst,
postrm = ctx.outputs.postrm,
preinst = ctx.outputs.preinst,
prerm = ctx.outputs.prerm,
)]
generate_scripts = rule(
implementation = _generate_scripts_impl,
attrs = {
"package_name": attr.string(mandatory = True),
"services": attr.string_list(),
"extra_postinst": attr.string_list(),
"add_user": attr.string(),
"user_groups": attr.string_list(),
"overriden_scripts": attr.label_list(allow_files = True),
"jinja2": attr.label(
default = Label("//tools:jinja2"),
cfg = "host",
executable = True,
allow_files = True,
),
"_postinst_template": attr.label(
default = Label("//rules/packaging/templates:postinst.jinja2"),
allow_single_file = True,
),
"_postrm_template": attr.label(
default = Label("//rules/packaging/templates:postrm.jinja2"),
allow_single_file = True,
),
"_preinst_template": attr.label(
default = Label("//rules/packaging/templates:preinst.jinja2"),
allow_single_file = True,
),
"_prerm_template": attr.label(
default = Label("//rules/packaging/templates:prerm.jinja2"),
allow_single_file = True,
),
},
outputs = {
"postinst": "%{package_name}.postinst",
"postrm": "%{package_name}.postrm",
"preinst": "%{package_name}.preinst",
"prerm": "%{package_name}.prerm",
},
)
def _pkg_deb_impl(ctx):
"""The implementation for the pkg_deb rule."""
files = [ctx.file.data]
args = [
"--output=" + ctx.outputs.deb.path,
"--changes=" + ctx.outputs.changes.path,
"--data=" + ctx.file.data.path,
"--package=" + ctx.attr.package,
"--architecture=" + ctx.attr.architecture,
"--maintainer=" + ctx.attr.maintainer,
]
if ctx.attr.scripts:
args.append("--preinst=@" + ctx.attr.scripts[ScriptsInfo].preinst.path)
files.append(ctx.attr.scripts[ScriptsInfo].preinst)
args.append("--postinst=@" + ctx.attr.scripts[ScriptsInfo].postinst.path)
files.append(ctx.attr.scripts[ScriptsInfo].postinst)
args.append("--prerm=@" + ctx.attr.scripts[ScriptsInfo].prerm.path)
files.append(ctx.attr.scripts[ScriptsInfo].prerm)
args.append("--postrm=@" + ctx.attr.scripts[ScriptsInfo].postrm.path)
files.append(ctx.attr.scripts[ScriptsInfo].postrm)
# Conffiles can be specified by a file or a string list
if ctx.attr.conffiles_file:
if ctx.attr.conffiles:
fail("Both conffiles and conffiles_file attributes were specified")
args.append("--conffile=@" + ctx.file.conffiles_file.path)
files.append(ctx.file.conffiles_file)
elif ctx.attr.conffiles:
args += ["--conffile=%s" % cf for cf in ctx.attr.conffiles]
# Version and description can be specified by a file or inlined
if ctx.attr.version_file:
if ctx.attr.version:
fail("Both version and version_file attributes were specified")
args.append("--version=@" + ctx.file.version_file.path)
files.append(ctx.file.version_file)
elif ctx.attr.version:
args.append("--version=" + ctx.attr.version)
else:
fail("Neither version_file nor version attribute was specified")
if ctx.attr.description_file:
if ctx.attr.description:
fail("Both description and description_file attributes were specified")
args.append("--description=@" + ctx.file.description_file.path)
files.append(ctx.file.description_file)
elif ctx.attr.description:
args.append("--description=" + ctx.attr.description)
else:
fail("Neither description_file nor description attribute was specified")
# Built using can also be specified by a file or inlined (but is not mandatory)
if ctx.attr.built_using:
args.append("--built_using=" + ctx.attr.built_using)
if ctx.attr.priority:
args.append("--priority=" + ctx.attr.priority)
if ctx.attr.section:
args.append("--section=" + ctx.attr.section)
if ctx.attr.homepage:
args.append("--homepage=" + ctx.attr.homepag)
args.append("--distribution=" + ctx.attr.distribution)
args.append("--urgency=" + ctx.attr.urgency)
args += ["--depends=" + d for d in ctx.attr.depends]
args += ["--suggests=" + d for d in ctx.attr.suggests]
args += ["--enhances=" + d for d in ctx.attr.enhances]
args += ["--conflicts=" + d for d in ctx.attr.conflicts]
args += ["--pre_depends=" + d for d in ctx.attr.predepends]
args += ["--recommends=" + d for d in ctx.attr.recommends]
args += ["--replaces=" + d for d in ctx.attr.replaces]
ctx.actions.run(
executable = ctx.executable.make_deb,
arguments = args,
inputs = files,
outputs = [ctx.outputs.deb, ctx.outputs.changes],
mnemonic = "MakeDeb",
)
# A rule for creating a deb file, see README.md
pkg_deb = rule(
implementation = _pkg_deb_impl,
attrs = {
"data": attr.label(mandatory = True, allow_single_file = True),
"package": attr.string(mandatory = True),
"architecture": attr.string(default = "all"),
"distribution": attr.string(default = "unstable"),
"urgency": attr.string(default = "medium"),
"maintainer": attr.string(mandatory = True),
"scripts": attr.label(mandatory = False),
"conffiles_file": attr.label(allow_single_file = True),
"conffiles": attr.string_list(default = []),
"version_file": attr.label(allow_single_file = True),
"version": attr.string(),
"description_file": attr.label(allow_single_file = True),
"description": attr.string(),
"built_using": attr.string(),
"priority": attr.string(),
"section": attr.string(),
"homepage": attr.string(),
"depends": attr.string_list(default = []),
"suggests": attr.string_list(default = []),
"enhances": attr.string_list(default = []),
"conflicts": attr.string_list(default = []),
"predepends": attr.string_list(default = []),
"recommends": attr.string_list(default = []),
"replaces": attr.string_list(default = []),
# Implicit dependencies.
"make_deb": attr.label(
default = Label("//rules/packaging:make_deb"),
cfg = "host",
executable = True,
allow_files = True,
),
},
outputs = {
"deb": "%{package}_%{version}_%{architecture}.deb",
"changes": "%{package}_%{version}_%{architecture}.changes",
},
executable = False,
)
def escape(x):
return '"%s"' % x
def _get_extensions():
extensions = {
".cron": "./etc/cron.d/",
".logrotate": "./etc/logrotate.d/",
".service": "./etc/systemd/system/",
}
debian_files = native.glob(["debian/**"])
overriden_scripts = []
content = {}
symlinks = {}
files_by_extension = {}
services = []
for filepath in debian_files:
if filepath in ["debian/postinst", "debian/preinst", "debian/prerm", "debian/postrm"]:
overriden_scripts.append(filepath)
filename = filepath[(filepath.rfind("/") + 1):]
last_dot = filepath.rfind(".")
fileext = filepath[last_dot:]
filepath_without_extension = filepath[:last_dot]
filename_without_extension = filepath_without_extension[(filepath_without_extension.rfind("/") + 1):]
if fileext in extensions:
content.setdefault(extensions[fileext], []).append(filepath)
# Cron-specific hack reasoned by these fucking monkeys restricting dots in the names of crontab files
if fileext == ".cron":
symlinks[extensions[".cron"] + filename_without_extension] = "./" + filename
if fileext == ".service":
services.append(filename_without_extension)
return {"content": content, "overriden_scripts": overriden_scripts, "services": services, "symlinks": symlinks}
def debian_package(
package_name,
version,
maintainer = default_maintainer,
architecture = "amd64",
description = None,
homepage = None,
depends = [],
replaces = [],
conflicts = [],
content = None,
symlinks = {},
owner = None,
mode = None,
owners = {},
modes = {},
extra_postinst = [],
target_hosts = [],
visibility = None):
"""Package creation, publishing and deployment targets
Args:
package_name (str): debian package name. Defaults to default_company_name + '-' + name
version (str): package version
maintainer (str): maintainer email
architecture (str): package architecture
description (str): package description
homepage (str): package homepage
depends (list): debian packages a created package depends on
replaces (list): debian packages a created package replaces
conflicts (list): debian packages a created package conflicts to
content (dict):
symlinks (dict):
owner (str):
mode (str):
owners (dict):
modes (dict):
extra_postinst (list):
target_hosts (list):
visibility (list):
"""
all_tars = []
extensions = _get_extensions()
if content == None:
content = {}
content.update(extensions["content"])
for path in content:
tar_name = package_name + "-" + path.replace("/", "-")
unstructured_tar_name = "unstructured-" + tar_name
content_for_path = content[path]
unstructured_content = [x for x in content_for_path if type(x) == "string"]
structured_content = [x for x in content_for_path if type(x) == "dict"]
pkg_tar(
name = unstructured_tar_name,
extension = "tar",
package_dir = path,
srcs = unstructured_content,
empty_dirs = [path] if not content_for_path else [],
ownername = owner,
mode = mode,
ownernames = owners,
modes = modes,
)
all_tars.append(unstructured_tar_name)
for i, el in enumerate(structured_content):
keep_structure = el.get("keep_structure", None)
strip_prefix = strip_prefix_by_action[keep_structure]
subtar_name = "structured-" + tar_name + "-" + str(i)
pkg_tar(
name = subtar_name,
extension = "tar",
package_dir = path,
srcs = el["srcs"],
strip_prefix = strip_prefix,
ownername = owner,
mode = mode,
ownernames = owners,
modes = modes,
)
all_tars.append(subtar_name)
scripts_target_name = package_name + "-scripts"
generate_scripts(
name = scripts_target_name,
package_name = package_name,
services = extensions["services"],
extra_postinst = extra_postinst,
overriden_scripts = extensions["overriden_scripts"],
)
data_tar_name = package_name + "-data-tar"
symlinks = {"." + k.lstrip("."): v for k, v in symlinks.items()}
symlinks.update(extensions["symlinks"])
pkg_tar(
name = data_tar_name,
deps = all_tars,
symlinks = symlinks,
visibility = visibility,
)
pkg_deb(
name = package_name + ".deb",
package = package_name,
version = version,
maintainer = maintainer,
architecture = architecture,
description = description,
homepage = homepage,
depends = depends,
replaces = replaces,
conflicts = conflicts,
data = data_tar_name,
scripts = scripts_target_name,
built_using = "bazel",
visibility = visibility,
)
deploy(
name = package_name + "-deploy",
package = package_name + ".deb",
target_hosts = target_hosts,
)
def pkg_files(
name,
srcs,
symlinks = {},
owner = "0.0",
ownername = None,
mode = "0555",
owners = {},
ownernames = {},
modes = {},
visibility = None):
"""Package creation, publishing and deployment targets
Args:
name (str): name
srcs (dict):
symlinks (dict):
owner (str):
ownername (str):
mode (str):
owners (dict):
ownernames (dict):
modes (dict):
visibility (dict):
"""
all_tars = []
for path in srcs:
tar_name = name + path.replace("/", "-")
partial_owner = owner
partial_ownername = ownername
partial_mode = mode
if path in owners:
partial_owner = owners[path]
if path in ownernames:
partial_ownername = ownernames[path]
if path in modes:
partial_mode = modes[path]
pkg_tar(
name = tar_name,
extension = "tar",
package_dir = path,
srcs = srcs[path],
empty_dirs = [path] if not srcs[path] else [],
owner = partial_owner,
ownername = partial_ownername,
mode = partial_mode,
strip_prefix = "./",
)
all_tars.append(tar_name)
symlinks = {"." + k.lstrip("."): v for k, v in symlinks.items()}
pkg_tar(
name = name,
deps = all_tars,
symlinks = symlinks,
owner = owner,
ownername = ownername,
mode = mode,
owners = owners,
ownernames = ownernames,
modes = modes,
visibility = visibility,
)