Installation

If you're using Arch Linux you can get pkger from AUR repositories with your favourite package manager like so:

  • paru pkger-rs

On other Linux distributions or MacOS download one of the latest prebuild static binaries from here. If your desired target is not on the list you'll have to build pkger from source by cloning the repository from https://github.com/vv9k/pkger and building it with:

  • cargo build --release

Configuration

By default pkger will look for the config file named .pkger.yml in the config directory appropriate for the OS that pkger is run on. If there is no global configuration, current directory will be scanned for the same file. To specify the location of the config file use --config or -c parameter.

The configuration file has a following structure:

# required
recipes_dir: ""
output_dir: ""

# optional
log_dir: ""
images_dir: ""
runtime_uri: "unix:///var/run/docker.sock"

# Disable colored output globally
no_color: true

ssh:
  # this will make the ssh auth socket available to the container so that it can use private keys from the host.
  forward_agent: true

  # This will allow tools that use SSH to connect to hosts that are not present in the `known_hosts` file
  disable_key_verification: true


# override default images used by pkger
custom_simple_images:
  deb: ubuntu:latest
  rpm: centos:latest

# To define custom images add the following
images:
  - name: rocky
    target: rpm
  - name: debian
    target: deb
# if pkger fails to find out the operating system you can specify it by os parameter
  - name: arch
    target: pkg
    os: Arch Linux

The required fields when running a build are recipes_dir and output_dir. First tells pkger where to look for recipes to build, the second is the directory where the final packages will end up.

When using custom images their location can be specified with images_dir.

If container runtime daemon that pkger should connect does not run on a default unix socket override the uri with runtime_uri parameter. pkger will automatically determine wether the provided runtime uri is a Podman or Docker daemon.

If an option is available as both configuration parameter and cli argument pkger will favour the arguments passed during startup.

Generate configuration file and directories

To quickly start of with pkger use the pkger init subcommand that will create necessary directories and the configuration file. Default locations can be overridden by command line parameters.

Recipes

Each recipe is a directory containing at least a recipe.yml or recipe.yaml file located at recipes_dir specified in the configuration.

The recipe is divided into 2 required (metadata, build) and 3 optional (config, install, env) parts. To read more on each topic select a subsection in the menu.

Here's an example working recipe for pkger:

metadata:
  name: pkger
  description: pkger
  arch: x86_64
  license: MIT
  version: 0.1.0
  maintainer: "vv9k"
  url: "https://github.com/vv9k/pkger"
  git: "https://github.com/vv9k/pkger.git"
  provides: [ pkger ]
  depends:
    pkger-deb: [ libssl-dev ]
    pkger-rpm: [ openssl-devel ]
  build_depends:
    all: [ gcc, pkg-config ]
    pkger-deb: [ curl libssl-dev ]
    pkger-rpm: [ cargo ]
    pkger-pkg: [ cargo ]
env:
  RUSTUP_URL: https://sh.rustup.rs
configure:
  steps:
    - cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL
      deb: true
    - cmd: sh /tmp/install_rust.sh -y --default-toolchain stable
      deb: true
build:
  steps:
    - cmd: cargo build --color=never
      rpm: true
      pkg: true
    - cmd: $HOME/.cargo/bin/cargo build --color=never
      deb: true
install:
  steps:
    - cmd: dir -p usr/bin
    - cmd: install -m755 $PKGER_BLD_DIR/target/debug/pkger usr/bin/

You can declare a new recipe with a subcommand. It will automatically create a directory in recipes_dir containing a recipe.yml with the generated YAML recipe:

$ pkger new recipe [OPTIONS] <NAME>

There is also a way to remove recipes. The remove subcommand erases the whole directory of a recipe if such exists:

$ pkger remove recipes <NAMES>...

# or shorhand
# or shorhand 'rm' for 'remove' and 'rcp' for 'recipes'
$ pkger rm rcp <NAMES>...

To see existing recipes use:

$ pkger list recipes

# or shorhand 'ls' for 'list' and 'rcp' for 'recipes'
$ pkger ls rcp <NAMES>...

# for more detailed output
$ pkger list -v recipes

Metadata (Required)

Contains all fields that describe the package being built.

required fields

metadata:
  name: pkger
  description: pkger
  license: MIT
  version: 0.1.0

optional fields

To specify which images a recipe should use add images parameter with a list of image targets. This field is ignored when building with --simple flag.

  images: [ rocky, debian ]

You can also specify that all images apply to this recipe with:

  all_images: true

sources

This fields are responsible for fetching the files used for the build. When both git and source are specified pkger will fetch both to the build directory.

If source starts with a prefix like http or https the file that if points to will be downloaded. If the file is an archive like .tar.gz or .tar.xz or .zip it will be directly extracted to $PKGER_BLD_DIR, otherwise the file will be copied to the directory untouched.

  source: "" # remote source or file system location

  # can also specify multiple sources:

  source:
    - 'http://some.website.com/file.tar.gz'
    - some_dir   # relative path will be prefixed with recipe directory
    - /some/absolute/path # can be a directory or a file

  git: https://github.com/vv9k/pkger.git # will default to branch = "master"

  # or specify a branch like this:
  git:
    url: https://github.com/vv9k/pkger.git
    branch: dev

Environment variables are available for this fields so this is possible:

  source: "https://github.com/vv9k/${RECIPE}/${RECIPE_VERSION}"

common

Optional fields shared across all targets.

  release: "1" # defaults to "0"

  epoch: "42"

  maintainer: "vv9k"

# The website of the package being built
  url: https://github.com/vv9k/pkger

  arch: x86_64 # defaults to `noarch` on RPM and `all` on DEB, `x86_64` automatically converted to `amd64` on DEB...

  skip_default_deps: true # skip installing default dependencies, it might break the builds

  exclude: ["share", "info"] # directories to exclude from final package

  group: "" # acts as Group in RPM or Section in DEB build

dependencies

Common fields that specify dependencies, conflicts and provides will be added to the spec of the final package.

This fields can be specified as arrays:

  depends: []
  conflicts: []
  provides: []

Or specified per image as a map below.

pkger will install all dependencies listed in build_depends, choosing an appropriate package manager for each supported distribution. Default dependencies like gzip or git might be installed depending on the target job type.

  build_depends:
    # common dependencies shared across all images
    all: ["gcc", "pkg-config", "git"]

    # dependencies for custom images
    rocky: ["cargo", "openssl-devel"]
    debian: ["curl", "libssl-dev"]

To specify same dependencies for multiple images join the images by + sign like this:

    rocky+fedora34: [ cargo, openssl-devel ]
    ubuntu20+debian: [ libssl-dev ]
    # you can later specify dependencies just for this images
    debian: [ curl ]

if running a simple build and there is a need to specify dependencies for the target add dependencies for one of this images:

    pkger-rpm+pkger-apk+pkger-pkg: ["cargo"]
    pkger-deb: ["curl"]
    pkger-gzip: []

A custom image, for example rocky, will also use dependecies defined for pkger-rpm. The same will apply for all rpm based images (or images that have their target specified to RPM in the configuration)

Patches

To apply patches to the fetched source code specify them just like dependencies. Patches can be specified as just file name in which case pkger will look for the patch in the recipe directory, if the path is absolute it will be read directly from the file system and finally if the patch starts with an http or https prefix the patch will be fetched from remote source.

  patches:
    - some-local.patch
    - /some/absolute/path/to.patch
    - https://someremotesource.com/other.patch
    - patch: with-strip-level.patch
      images: [ debian ] # specify the images that this patch should be aplied on
      strip: 2 # this specifies the number of directories to strip before applying the patch (known as -pN or --stripN option in UNIX patch tool

RPM fields

Optional fields that will be used when building RPM target.

  rpm:
    vendor: ""
    icon: ""
    summary: "shorter description" # if not provided defaults to value of `description`
    config_noreplace: "%{_sysconfdir}/%{name}/%{name}.conf"

    pre_script: ""
    post_script: ""
    preun_script: ""
    postun_script: ""
    
    # Disable automatic dependency processing. Setting this to true has no effect.
    auto_req_prov: false

    # acts the same as other dependencies - can be passed as array
    #obsoletes: ["foo"]
    # or as a map
    obsoletes:
      rocky: ["foo"]

DEB fields

Optional fields that may be used when building a DEB package.

  deb:
    priority: ""
    built_using: ""
    essential: true
    
    # specify the content of post install script
    postinst: ""

    # same as all other dependencies but deb specific
    pre_depends: []
    recommends: []
    suggests: []
    breaks: []
    replaces: []
    enhances: []

PKG fields

Optional fields that will be used when building a PKG package.

  pkg:
    # location of the script in `$PKGER_OUT_DIR` that contains pre/post install/upgrade/remove functions
    # to be included in the final pkg
    install: ".install"
    
    # A list of files to be backed up when package will be removed or upgraded
    backup: ["/etc/pkger.conf"]
    
    # A list of packages that this package replaces
    replaces: []
    
    # This are dependencies that this package needs to offer full functionality.
    optdepends:
      # Each dependency should contain a short description in this format:
      - "libpng: PNG images support"
      - "alsa-lib: sound support"

APK fields

Optional fields that will be used when building a APK package.

  apk:
    install: "$pkgname.pre-install $pkgname.post-install"
    
    # A list of packages that this package replaces
    replaces: []

    # A list of dependencies for the check phase
    checkdepends: []

    # If not provided a new generated key will be used to
    # sign the package
    private_key: "/location/of/apk_signing_key"

Scripts

pkger has 3 defined build phases - configure, build and install of which only build is required to create a package.

Each phase has field called steps that takes an array of steps to execute during a given phase. A step can be a simple string that will be executed in the default shell like "echo 123" or an entry that specifies on what targets it should be executed like:

    - cmd: >-
        echo only on deb targets
      deb: true

To set a working directory during the script phase set the working_dir parameter like so:

  working_dir: /tmp

Environment variables are available for this field so this is possible:

  working_dir: ${PKGER_BLD_DIR}/${RECIPE}-${RECIPE_VERSION}-${SOME_USER_DEFINED_VAR}

To use a different shell to execute each command set the shell parameter:

  shell: "/bin/bash" # optionally change default `/bin/sh`

configure (Optional)

Optional configuration steps. If provided the steps will be executed before the build phase. The working directory will be set to $PKGER_BLD_DIR

configure:
  shell: "/bin/bash"
  steps:
    - cmd: >-
        curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

build (Required)

This is the phase where the package should be assembled/compiled/linked and so on. All steps executed during the build will have the working directory seto to $PKGER_BLD_DIR. This directory will contain either extracted sources if source is specified in metadata or a git repository if git was specified.

build:
  steps:
    - cmd: $HOME/.cargo/bin/cargo build --release .
    - images: [ debian ]
      cmd: echo 'hello from Debian' # will only be executed on image `debian`

    - cmd: echo 'will only run on images with target == `rpm`'
      rpm: true
    # same applies to other targets
      pkg: false
      apk: false
      deb: false
      gzip: falze


    - cmd: echo 'only on version 0.2.0'
      versions: [ 0.2.0 ]
    # fields can also be combined
    - cmd: echo 'only on debian version 0.2.0'
      versions: [ 0.2.0 ]
      images: [ debian ]
]

install (Optional)

Optional steps that (if provided) will be executed after the build phase. Working directory of each step will be set to $PKGER_OUT_DIR so you can use relative paths with commands like install. Each file that ends up in $PKGER_OUT_DIR will be available in the final package unless explicitly excluded by exclude field in metadata. So in the example below, the file that is installed will be available as /usr/bin/pkger with permissions preserved.

install:
  steps:
    - cmd: >-
        install -m755 $PKGER_BLD_DIR/target/release/pkger usr/bin/pkger

env (Optional)

Optional environment variables that should be available during the scripts phase and in some metadata fields.

env:
  HTTPS_PROXY: http://proxy.domain.com:1234
  RUST_LOG: trace

pkger variables

Some variables will be available to use during the build like:

  • $PKGER_OS the distribution of current container
  • $PKGER_OS_VERSION version of the distribution if applies
  • $PKGER_BLD_DIR the build directory with fetched source or git repo in the container
  • $PKGER_OUT_DIR the final directory from which pkger will copy files to target package
  • $RECIPE the name of the recipe that is built
  • $RECIPE_VERSION the version of the recipe
  • $RECIPE_RELEASE the release of the recipe

Recipes inheritance

Recipes support inheriting fields from a defined base recipe to avoid repetition. For example here is a definition of a base package:

---
metadata:
  name: base-package
  version: 0.1.0
  description: pkger base package testing recipe inheritance
  arch: x86_64
  license: MIT
  images: [ rocky, debian ]
build:
  working_dir: $PKGER_OUT_DIR
  steps:
    - cmd: echo 123 >> ${RECIPE}_${RECIPE_VERSION}

And here is a child recipe using from field to define the parent recipe:

---
from: base-package
metadata:
  name: child-package1
  version: 0.2.0
  description: pkger child package testing recipe inheritance from base-package
install:
  shell: /bin/bash
  steps:
    - cmd: >-
        if [[ $(cat ${RECIPE}_${RECIPE_VERSION}) =~ 123 ]]; then exit 0; else
        echo "Test file ${RECIPE}_${RECIPE_VERSION} has invalid content"; exit 1; fi

The child-package1 will share the build steps as well as arch, license, images fields. After merging the child recipe will look something like this:

---
from: base-package
metadata:
  name: child-package1
  version: 0.2.0
  description: pkger child package testing recipe inheritance from base-package
  arch: x86_64
  license: MIT
  images: [ rocky, debian ]
build:
  working_dir: $PKGER_OUT_DIR
  steps:
    - cmd: echo 123 >> ${RECIPE}_${RECIPE_VERSION}
install:
  shell: /bin/bash
  steps:
    - cmd: >-
        if [[ $(cat ${RECIPE}_${RECIPE_VERSION}) =~ 123 ]]; then exit 0; else
        echo "Test file ${RECIPE}_${RECIPE_VERSION} has invalid content"; exit 1; fi

When defining a child recipe only from and metadata.name fields are required. Here is a minimal child recipe:

---
from: base-package
metadata:
  name: child-package2

For a working example refer to the example directory of pkger source tree.

Images

Images are an optional feature of pkger.By default pkger will create necessary images to build simple targets, they are completely distinct from user defined ones. User images offer higher customisation when it comes to preparing the build environment.

In the images directory specified by the configuration pkger will treat each subdirectory containing a Dockerfile as an image. The name of the directory will become the name of the image.

So example structure like this:

images
├── arch
│  └── Dockerfile
├── rocky
│  └── Dockerfile
└── debian
   └── Dockerfile

pkger will detect 3 images - arch, rocky and debian.

Images with dependencies installed will be cached for each recipe-target combo to reduce the number of times the dependencies have to be pulled from remote sources. This saves a lot of space, time and bandwith.

You can declare a new image with a subcommand. It will automatically create a directory in images_dir containing an empty Dockerfile.

$ pkger new image <NAME>

There is also a way to remove images. The remove subcommand erases the whole directory of an image if such exists:

$ pkger remove images <NAMES>...

# or shorhand 'rm' for 'remove' and 'img' for 'images'
$ pkger rm img <NAMES>...

To see existing images use:

$ pkger list images

# or shorhand 'ls' for 'list' and 'img' for 'images'
$ pkger ls img

# for more detailed output
$ pkger list -v images

Build a package

Currently available targets are: rpm, deb, pkg, apk, gzip.

Simple build

To build a simple package using pkger use:

pkger build --simple [TARGETS] -- [RECIPES]

When using a simple build following linux distributions will be used for build images:

  • rpm: rockylinux/rockylinux:latest
  • deb: debian:latest
  • pkg: archlinux
  • apk: alpine:latest
  • gzip: debian:latest

To override the default images set custom_simple_images like this:

custom_simple_images:
  deb: ubuntu:18
  rpm: fedora:latest

Custom images build

To use custom images drop the --simple parameter and just use:

pkger build [RECIPES]

For this to have any effect the recipes have to have image targets defined (more on that here)

Examples

Build a recipe for all supported images:

pkger build recipe

# or shorthand 'b' for 'build'
pkger b recipe

Build all recipes for all supported images

pkger build --all

Build multiple recipes on specified custom images:

pkger build -i custom-image1 custom-image2 -- recipe1 recipe2

Build simple RPM, DEB, PKG... packages:

pkger build -s rpm -s deb -s pkg -s gzip -- recipe1

Build only RPM package:

pkger build -s rpm -- recipe1

Output

After successfully building a package pkger will put the output artifact to output_dir specified in configuration joined by the image name that was used to build the package. Each image will have a separate directory with all of its output packages.

Signing

To sign packages automatically using a GPG key add the following to your configuration file:

gpg_key: /absolute/path/to/the/private/key
gpg_name: Packager Name # must be the same as the `Name` field on the key

When pkger detects the gpg key in the configuration it will prompt for a password to the key on each run.

Currently, only deb and rpm targets support signing.

Formatting output

By default pkger will display basic output as hierhical log with level set to INFO. All log messages will be printed to stdout unless a --log-dir flag (or log_dir is specified in configuration) is provided, in that case there will be a single global log file in the logging directory created on each run as well as a separate file for each task.

To debug run with -d or --debug option. To surpress all output except for errors and warnings add -q or --quiet. To enable very verbose output add -t or `--trace option.

Generate recipes

To generate a recipe declaratively from CLI use the pkger new recipe subcommand. By default it requires only the name of the package and creates a directory with recipe.yml in it.

Create images

To create images use pkger new image <name>. This will create a directory with a Dockerfile in the images_dir specified in the configuration.

Edit recipes, images and config

pkger provides utility subcommand edit that invokes the default editor defined by $EDITOR environment variable. To make this functionality work, export this variable in your shell's init script like ~/.bashrc.

Edit images and recipes by name:

# This will open up the Dockerfile in the `rocky` image.
$ pkger edit image rocky 

# This will open up the `recipe.yml` or `recipe.yaml` file in `pkger-simple` recipe directory
$ pkger edit recipe pkger-simple

To edit the configuration file run:

$ pkger edit config


# or shorhand 'e' for 'edit'
$ pkger e <object>

Shell completions

pkger provides a subcommand to print shell completions. Supported shells are: bash, zsh, fish, powershell, elvish.

To print the completions run:

pkger print-completions bash

replacing bash with whatever shell you prefer.

To have completions automatically add something along those lines to your .bashrc, .zshrc...:

. <(pkger print-completions bash)