Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mattj23 committed Nov 2, 2022
0 parents commit f52c11f
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea/
__pycache__/
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Debian Package Generation Tool

Debian packaging tool for building binary .deb files and optionally pushing them to *aptly* repositories. Designed for use in CI/CD build pipelines.

## Usage

Create a build context. No files get moved or created until the build step, rather a working context is saved in `~/.deb-pack.json` and during the build step everything is performed in a temporary directory.

```bash
pack create

# Or
pack create --from <path-to-folder>
```

Empty file added pack/__init__.py
Empty file.
67 changes: 67 additions & 0 deletions pack/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import json
import os
from dataclasses import dataclass, asdict
from typing import Dict, List


@dataclass
class Target:
source_path: str
target_path: str


@dataclass
class Context:
control: Dict[str, str]
targets: List[Target]

def populate(self, path: str):
for root, folders, files in os.walk(path):
for file in files:
absolute = os.path.abspath(os.path.join(root, file))
if file == "control" and os.path.split(root)[-1] == "DEBIAN":
with open(absolute, "r") as handle:
items = [x.split(":", 1) for x in handle.read().split("\n") if ":" in x]
values = {key.strip(): value.strip() for key, value in items}
self.control.update(values)

print("control file", absolute)
else:
relative = os.path.relpath(absolute, path)
self.targets.append(Target(absolute, relative))

def save(self, path: str):
with open(path, "w") as handle:
json.dump(asdict(self), handle, indent=2)

def add_target(self, source, dest):
self.targets.append(Target(source, dest))

def built_name(self) -> str:
errors = []
values = {}
for key in ("Package", "Version", "Architecture"):
if key not in self.control:
errors.append(key)
else:
values[key] = self.control[key]
if errors:
raise KeyError(f"Missing the following control keys: {', '.join(errors)}")

return "{Package}_{Version}_{Architecture}.deb".format(**values)


def create_context() -> Context:
return Context({}, [])


def load_context(path: str) -> Context:
if not os.path.exists(path):
raise FileNotFoundError()

with open(path, "r") as handle:
raw = json.load(handle)

targets = [Target(**x) for x in raw["targets"]]

return Context(raw["control"], targets)
135 changes: 135 additions & 0 deletions pack/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import os
import shutil
import pathlib
import click
import sys
import subprocess
from aptly_api import Client
from tempfile import TemporaryDirectory

from pack.context import Context, create_context, load_context

_context_file = os.path.join(pathlib.Path.home(), ".deb-pack.json")


@click.group()
def main():
pass


@main.command()
def build():
try:
context = load_context(_context_file)
except FileNotFoundError:
click.echo(f"There is no active working context!")
sys.exit(1)

try:
output_name = context.built_name()
except KeyError as e:
click.echo(e)
sys.exit(1)

if os.path.exists(output_name):
click.echo(f"Package {output_name} already exists, doing nothing")

click.echo(f"Building {output_name}...")
output_path, _ = os.path.splitext(output_name)

with TemporaryDirectory() as directory:
root = os.path.join(directory, output_path)
deb_folder = os.path.join(root, "DEBIAN")
os.makedirs(deb_folder)

for target in context.targets:
destination = os.path.join(root, target.target_path)
dest_path, _ = os.path.split(destination)
if not os.path.exists(dest_path):
os.makedirs(dest_path)
click.echo(f" > {destination}")
shutil.copy(target.source_path, destination)

with open(os.path.join(deb_folder, "control"), "w") as handle:
handle.write("\n".join(f"{key}: {value}" for key, value in context.control.items()))
handle.write("\n")

subprocess.call(["dpkg-deb", "--build", "--root-owner-group", root])
built = os.path.join(directory, output_name)
if not os.path.exists(built):
click.echo("No file was built!")
sys.exit(1)

shutil.copy(built, output_name)
click.echo(f"Final file copied to {os.path.abspath(output_name)}")
click.echo("Done")


@main.command()
def show():
try:
context = load_context(_context_file)
except FileNotFoundError:
click.echo(f"There is no active working context!")
sys.exit(1)

click.echo("Control data:")
for k, v in context.control.items():
click.echo(f" {k}: {v}")

click.echo("\nTargets:")
for item in context.targets:
click.echo(f"{item.source_path} -> {item.target_path}")


@main.command()
@click.argument("key", type=str)
@click.argument("value", type=str)
def control(key, value):
try:
context = load_context(_context_file)
except FileNotFoundError:
click.echo(f"There is no active working context!")
sys.exit(1)

context.control[key] = value
click.echo(f"Setting control '{key}': {value}")
context.save(_context_file)


@main.command()
@click.argument("source_path", type=click.Path(exists=True))
@click.argument("destination_path", type=click.Path())
@click.option("-n", "--name", type=str)
def add(source_path, destination_path, name):
try:
context = load_context(_context_file)
except FileNotFoundError:
click.echo(f"There is no active working context!")
sys.exit(1)

absolute = os.path.abspath(source_path)
if not name:
_, name = os.path.split(source_path)
dest = os.path.join(destination_path, name)
if dest.startswith("/"):
dest = dest[1:]

context.add_target(absolute, dest)
click.echo(f"Adding target: {absolute} -> {dest}")
context.save(_context_file)


@main.command()
@click.option("-f", "--from", "from_path")
def create(from_path):
click.echo(f"Creating at {os.getcwd()}")
context = create_context()
if from_path:
context.populate(from_path)

context.save(_context_file)


if __name__ == '__main__':
main()
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build-system]
requires = [
"setuptools>45", "wheel"
]
build-backend="setuptools.build_meta"
31 changes: 31 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import setuptools

with open("README.md", "r", encoding="utf-8") as handle:
long_description = handle.read()

setuptools.setup(
name="deb-pack",
version="0.1.0",
author="Matthew Jarvis",
author_email="[email protected]",
description="Debian packaging tool",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/mattj23/deb-pack",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.8',
install_requires=[
"click>=8.1.3",
"aptly-api-client>=0.2.4"
],
entry_points={
"console_scripts": [
"pack=pack.main:main",
]
}
)

0 comments on commit f52c11f

Please sign in to comment.