mirror of
https://github.com/nexus-stc/hyperboria
synced 2024-12-23 18:17:45 +01:00
8472f27ec5
GitOrigin-RevId: ddf02e70d2827c048db49b687ebbcdcc67807ca6
557 lines
19 KiB
Python
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,
|
|
)
|