├── .codespell-ignore ├── .sphinx ├── .markdownlint │ ├── exceptions.txt │ ├── style.rb │ ├── doc-lint.sh │ └── rules.rb ├── _extra │ └── versions.json ├── _templates │ ├── sidebar │ │ └── variant-selector.html │ ├── page.html │ └── footer.html ├── _static │ ├── header-nav.js │ └── version-switcher.js ├── .spellcheck.yaml ├── requirements.txt └── wordlist.txt ├── testdata ├── testfile ├── testfile.gpg ├── testfile.sig ├── testfile.asc └── testfile-invalid.asc ├── distrobuilder ├── main_repack-windows_test.go ├── main_validate.go ├── passwd.go ├── vm_test.go ├── chroot.go └── main_build-dir.go ├── doc ├── substitutions.yaml ├── tutorials │ └── index.md ├── howto │ ├── index.md │ ├── install.md │ └── troubleshoot.md ├── reference │ ├── command_line_options.md │ ├── index.md │ ├── filters.md │ ├── mappings.md │ ├── image.md │ ├── targets.md │ ├── actions.md │ ├── source.md │ └── packages.md ├── index.md └── examples │ └── sabayon.yaml ├── mkdocs.yml ├── shared ├── version │ └── version.go ├── logger.go ├── osarch_test.go ├── archive_linux.go └── osarch.go ├── test └── lint │ ├── codespell.sh │ ├── no-oneline-assign-and-test.sh │ ├── mixed-whitespace.sh │ ├── trailing-space.sh │ ├── no-short-form-imports.sh │ ├── negated-is-bool.sh │ └── newline-after-block.sh ├── .gitignore ├── AUTHORS ├── windows ├── testdata │ ├── inf01.txt │ ├── inf02.txt │ ├── inf03.txt │ ├── winpe_boot_wim_info.txt │ ├── w8_install_wim_info.txt │ ├── 2k12r2_install_wim_info.txt │ └── w10_boot_wim_info.txt ├── drivers.go ├── inf_test.go ├── inf.go ├── driver_netkvm.go ├── driver_viogpudo.go └── wiminfo.go ├── sources ├── archlinux-http_test.go ├── apertis-http_test.go ├── openwrt-http_test.go ├── nixos-http.go ├── rootfs-http.go ├── openeuler-http_test.go ├── testdata │ ├── key5.pub │ ├── key2.pub │ └── key4.pub ├── alpaquita-http.go ├── alt-http.go ├── rpmbootstrap.go ├── funtoo-http.go ├── source.go ├── busybox.go ├── debootstrap.go ├── vyos-http.go ├── apertis-http.go ├── common_test.go ├── docker.go ├── voidlinux-http.go └── archlinux-http.go ├── managers ├── portage.go ├── egoportage.go ├── slackpkg.go ├── xbps.go ├── opkg.go ├── custom.go ├── dnf.go ├── zypper.go ├── manager_test.go ├── equo.go ├── anise.go ├── apk.go ├── common.go ├── yum.go ├── pacman.go └── apt.go ├── .github └── workflows │ ├── builds.yml │ ├── commits.yml │ └── tests.yml ├── generators ├── remove.go ├── common.go ├── utils.go ├── generators_test.go ├── fstab.go ├── generators.go ├── dump.go ├── template.go ├── hostname_test.go ├── hosts_test.go ├── hostname.go ├── template_test.go ├── hosts.go └── dump_test.go ├── .golangci.yml ├── CONTRIBUTING.md ├── Makefile └── README.md /.codespell-ignore: -------------------------------------------------------------------------------- 1 | buildd 2 | -------------------------------------------------------------------------------- /.sphinx/.markdownlint/exceptions.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/testfile: -------------------------------------------------------------------------------- 1 | I need to be verified. 2 | -------------------------------------------------------------------------------- /distrobuilder/main_repack-windows_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /testdata/testfile.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxc/distrobuilder/HEAD/testdata/testfile.gpg -------------------------------------------------------------------------------- /testdata/testfile.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxc/distrobuilder/HEAD/testdata/testfile.sig -------------------------------------------------------------------------------- /doc/substitutions.yaml: -------------------------------------------------------------------------------- 1 | # Key/value substitutions to use within the Sphinx doc. 2 | {example_key: "Value"} 3 | -------------------------------------------------------------------------------- /.sphinx/_extra/versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "latest", 4 | "id": "latest" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "distrobuilder - system container and VM image builder" 2 | theme: readthedocs 3 | docs_dir: doc 4 | -------------------------------------------------------------------------------- /shared/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version contains the distrobuilder version number. 4 | var Version = "3.2" 5 | -------------------------------------------------------------------------------- /test/lint/codespell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | codespell -I .codespell-ignore distrobuilder doc generators image managers shared sources test windows 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sphinx 2 | doc/html/ 3 | .sphinx/deps/ 4 | .sphinx/themes/ 5 | .sphinx/venv/ 6 | .sphinx/warnings.txt 7 | .sphinx/.wordlist.dic 8 | .sphinx/_static/download 9 | -------------------------------------------------------------------------------- /doc/tutorials/index.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | These tutorials guide you through the usage of `distrobuilder`. 4 | 5 | ```{toctree} 6 | :titlesonly: 7 | 8 | use.md 9 | ``` 10 | -------------------------------------------------------------------------------- /doc/howto/index.md: -------------------------------------------------------------------------------- 1 | # How-to guides 2 | 3 | These how-to guides cover key operations and processes in `distrobuilder`. 4 | 5 | ```{toctree} 6 | :titlesonly: 7 | 8 | install.md 9 | build.md 10 | troubleshoot.md 11 | ``` 12 | -------------------------------------------------------------------------------- /.sphinx/_templates/sidebar/variant-selector.html: -------------------------------------------------------------------------------- 1 |
2 | Doc version: 3 |
4 |
5 | -------------------------------------------------------------------------------- /test/lint/no-oneline-assign-and-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | echo "Checking for oneline assign & test..." 4 | 5 | # Recursively grep go files for if statements that contain assignments. 6 | ! git grep --untracked -P -n '^\s+if.*:=.*;.*{\s*$' -- '*.go' 7 | -------------------------------------------------------------------------------- /doc/reference/command_line_options.md: -------------------------------------------------------------------------------- 1 | # Command line options 2 | 3 | % Include content from [../../README.md](../../README.md) 4 | ```{include} ../../README.md 5 | :start-after: 6 | :end-before: 7 | ``` 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Unless mentioned otherwise in a specific file's header, all code in this 2 | project is released under the Apache 2.0 license. 3 | 4 | The list of authors and contributors can be retrieved from the git 5 | commit history and in some cases, the file headers. 6 | -------------------------------------------------------------------------------- /doc/howto/install.md: -------------------------------------------------------------------------------- 1 | # How to install `distrobuilder` 2 | 3 | % Include content from [../../README.md](../../README.md) 4 | ```{include} ../../README.md 5 | :start-after: 6 | :end-before: 7 | ``` 8 | -------------------------------------------------------------------------------- /test/lint/mixed-whitespace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | echo "Checking for mixed tabs and spaces in shell scripts..." 4 | 5 | OUT=$(git grep --untracked -lP '\t' '*.sh' || true) 6 | if [ -n "${OUT}" ]; then 7 | echo "ERROR: mixed tabs and spaces in script: ${OUT}" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /test/lint/trailing-space.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | echo "Checking that there are no trailing spaces in shell scripts..." 4 | 5 | OUT=$(git grep --untracked -lP "\s$" '*.sh' || true) 6 | if [ -n "${OUT}" ]; then 7 | echo "ERROR: trailing space in script: ${OUT}" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /test/lint/no-short-form-imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | echo "Checking for short form imports..." 4 | 5 | OUT=$(git grep --untracked -n -P '^\s*import\s+"' '*.go' | grep -v ':import "C"$' || true) 6 | if [ -n "${OUT}" ]; then 7 | echo "ERROR: found short form imports: ${OUT}" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /doc/reference/index.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | The reference material in this section provides technical descriptions of `distrobuilder`. 4 | 5 | ```{toctree} 6 | :titlesonly: 7 | 8 | actions 9 | command_line_options 10 | filters 11 | generators 12 | image 13 | mappings 14 | packages 15 | source 16 | targets 17 | ``` 18 | -------------------------------------------------------------------------------- /.sphinx/_static/header-nav.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $(document).on("click", function () { 3 | $(".more-links-dropdown").hide(); 4 | }); 5 | 6 | $('.nav-more-links').click(function(event) { 7 | $('.more-links-dropdown').toggle(); 8 | event.stopPropagation(); 9 | }); 10 | }) 11 | -------------------------------------------------------------------------------- /test/lint/negated-is-bool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | echo "Checking usage of negated shared.Is(True|False)*() functions..." 4 | 5 | OUT=$(git grep --untracked -P '!(shared\.)?Is(True|False).*\(' '*.go' || true) 6 | if [ -n "${OUT}" ]; then 7 | echo "ERROR: negated shared.Is(True|False)*() function in script: ${OUT}" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /.sphinx/.markdownlint/style.rb: -------------------------------------------------------------------------------- 1 | all 2 | exclude_rule 'MD013' 3 | exclude_rule 'MD046' 4 | exclude_rule 'MD041' 5 | exclude_rule 'MD040' 6 | exclude_rule 'MD024' 7 | exclude_rule 'MD033' 8 | exclude_rule 'MD022' 9 | exclude_rule 'MD031' 10 | rule 'MD026', :punctuation => '.,;:!' 11 | rule 'MD003', :style => :atx 12 | rule 'MD007', :indent => 3 13 | -------------------------------------------------------------------------------- /windows/testdata/inf01.txt: -------------------------------------------------------------------------------- 1 | [Version] 2 | Signature="$Windows NT$" 3 | Class=SCSIAdapter 4 | ClassGUID={4D36E97B-E325-11CE-BFC1-08002BE10318} 5 | Provider=%VENDOR% 6 | DriverVer = 02/25/2024,100.94.104.24800 7 | CatalogFile=viostor.cat 8 | DriverPackageType = PlugAndPlay 9 | DriverPackageDisplayName = %VioStorScsi.DeviceDesc% 10 | PnpLockdown=1 11 | -------------------------------------------------------------------------------- /sources/archlinux-http_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestArchLinuxGetLatestRelease(t *testing.T) { 12 | src := &archlinux{} 13 | src.client = http.DefaultClient 14 | 15 | release, err := src.getLatestRelease("https://archive.archlinux.org/iso/", "x86_64") 16 | require.NoError(t, err) 17 | require.Regexp(t, regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}$`), release) 18 | } 19 | -------------------------------------------------------------------------------- /.sphinx/.spellcheck.yaml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown files 3 | aspell: 4 | lang: en 5 | d: en_US 6 | dictionary: 7 | wordlists: 8 | - .sphinx/wordlist.txt 9 | output: .sphinx/.wordlist.dic 10 | sources: 11 | - doc/html/**/*.html 12 | pipeline: 13 | - pyspelling.filters.html: 14 | comments: false 15 | attributes: 16 | - title 17 | - alt 18 | ignores: 19 | - code 20 | - pre 21 | - spellexception 22 | - link 23 | - title 24 | - div.relatedlinks 25 | -------------------------------------------------------------------------------- /shared/logger.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // GetLogger returns a new logger. 10 | func GetLogger(debug bool) (*logrus.Logger, error) { 11 | logger := logrus.StandardLogger() 12 | 13 | logger.SetOutput(os.Stdout) 14 | 15 | formatter := logrus.TextFormatter{ 16 | FullTimestamp: true, 17 | PadLevelText: true, 18 | } 19 | 20 | formatter.EnvironmentOverrideColors = true 21 | 22 | logger.Formatter = &formatter 23 | 24 | if debug { 25 | logger.Level = logrus.DebugLevel 26 | } 27 | 28 | return logger, nil 29 | } 30 | -------------------------------------------------------------------------------- /managers/portage.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | type portage struct { 4 | common 5 | } 6 | 7 | func (m *portage) load() error { 8 | m.commands = managerCommands{ 9 | clean: "emerge", 10 | install: "emerge", 11 | refresh: "true", 12 | remove: "emerge", 13 | update: "emerge", 14 | } 15 | 16 | m.flags = managerFlags{ 17 | global: []string{}, 18 | clean: []string{}, 19 | install: []string{ 20 | "--autounmask-continue", 21 | }, 22 | remove: []string{ 23 | "--unmerge", 24 | }, 25 | refresh: []string{}, 26 | update: []string{ 27 | "--update", "@world", 28 | }, 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /windows/drivers.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | // DriverInfo contains driver specific information. 4 | type DriverInfo struct { 5 | PackageName string 6 | SoftwareRegistry string 7 | SystemRegistry string 8 | DriversRegistry string 9 | } 10 | 11 | // Drivers contains all supported drivers. 12 | var Drivers = map[string]DriverInfo{ 13 | "Balloon": driverBalloon, 14 | "NetKVM": driverNetKVM, 15 | "vioinput": driverVioinput, 16 | "viorng": driverViorng, 17 | "vioscsi": driverVioscsi, 18 | "vioserial": driverVioserial, 19 | "viofs": driverViofs, 20 | "viogpudo": driverVioGPUDo, 21 | "viostor": driverViostor, 22 | "viosock": driverViosock, 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/builds.yml: -------------------------------------------------------------------------------- 1 | name: Builds 2 | on: 3 | - push 4 | - pull_request 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | doc: 11 | name: Documentation (Sphinx) 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Build docs 18 | run: make doc 19 | 20 | - name: Print warnings 21 | run: if [ -s .sphinx/warnings.txt ]; then cat .sphinx/warnings.txt; exit 1; fi 22 | 23 | - name: Upload artifacts 24 | if: always() 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: documentation 28 | path: doc/html 29 | -------------------------------------------------------------------------------- /managers/egoportage.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | type egoportage struct { 4 | common 5 | } 6 | 7 | func (m *egoportage) load() error { 8 | m.commands = managerCommands{ 9 | clean: "emerge", 10 | install: "emerge", 11 | refresh: "ego", 12 | remove: "emerge", 13 | update: "emerge", 14 | } 15 | 16 | m.flags = managerFlags{ 17 | global: []string{}, 18 | clean: []string{}, 19 | install: []string{ 20 | "--autounmask-continue", 21 | "--quiet-build=y", 22 | }, 23 | remove: []string{ 24 | "--unmerge", 25 | }, 26 | refresh: []string{ 27 | "sync", 28 | }, 29 | update: []string{ 30 | "--update", "@world", 31 | }, 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /sources/apertis-http_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestApertisHTTP_getLatestRelease(t *testing.T) { 12 | s := &apertis{} 13 | s.client = http.DefaultClient 14 | 15 | tests := []struct { 16 | release string 17 | want string 18 | }{ 19 | { 20 | "18.12", 21 | "18.12.0", 22 | }, 23 | } 24 | 25 | for _, tt := range tests { 26 | baseURL := fmt.Sprintf("https://images.apertis.org/release/%s", tt.release) 27 | release, err := s.getLatestRelease(baseURL, tt.release) 28 | require.NoError(t, err) 29 | require.Equal(t, tt.want, release) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc/reference/filters.md: -------------------------------------------------------------------------------- 1 | # Filters 2 | 3 | Filters can be used to restrict certain sections from being run or being applied. 4 | There are three filters, `releases`, `architectures`, and `variants`, and each filter takes a list. 5 | 6 | Here's an example: 7 | 8 | ```yaml 9 | releases: 10 | - v1 11 | - v2 12 | architectures: 13 | - x86_64 14 | variants: 15 | - cloud 16 | ``` 17 | 18 | In the above case, the section will only be applied or run if the release is v1 or v2, the architecture is x86_64 _and_ the variant is cloud. 19 | 20 | Filters can be applied to each item individually in the lists of following sections: 21 | 22 | - files 23 | - sets (packages) 24 | - actions 25 | - repositories 26 | -------------------------------------------------------------------------------- /generators/remove.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/lxc/distrobuilder/image" 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | type remove struct { 12 | common 13 | } 14 | 15 | // RunLXC removes a path. 16 | func (g *remove) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error { 17 | return g.Run() 18 | } 19 | 20 | // RunIncus removes a path. 21 | func (g *remove) RunIncus(img *image.IncusImage, target shared.DefinitionTargetIncus) error { 22 | return g.Run() 23 | } 24 | 25 | // Run removes a path. 26 | func (g *remove) Run() error { 27 | return os.RemoveAll(filepath.Join(g.sourceDir, g.defFile.Path)) 28 | } 29 | -------------------------------------------------------------------------------- /.sphinx/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster 2 | Babel 3 | certifi 4 | charset-normalizer 5 | colorama 6 | docutils 7 | idna 8 | imagesize 9 | Jinja2 10 | livereload 11 | MarkupSafe 12 | packaging 13 | Pygments 14 | pyparsing 15 | pytz 16 | requests 17 | six 18 | snowballstemmer 19 | Sphinx 20 | sphinx-autobuild 21 | sphinxcontrib-applehelp 22 | sphinxcontrib-devhelp 23 | sphinxcontrib-htmlhelp 24 | sphinxcontrib-jsmath 25 | sphinxcontrib-qthelp 26 | sphinxcontrib-serializinghtml 27 | sphinxcontrib-jquery 28 | tornado 29 | urllib3 30 | myst-parser<3.0.0 31 | sphinx-tabs 32 | sphinx-reredirects 33 | linkify-it-py 34 | furo 35 | sphinxext-opengraph>=0.6.1 36 | lxd-sphinx-extensions 37 | pyspelling 38 | sphinx-design 39 | -------------------------------------------------------------------------------- /windows/inf_test.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMatchClassGuid(t *testing.T) { 9 | tcs := []struct { 10 | filename string 11 | want string 12 | }{ 13 | {"testdata/inf01.txt", "{4D36E97B-E325-11CE-BFC1-08002BE10318}"}, 14 | {"testdata/inf02.txt", "{4D36E97B-E325-11CE-BFC1-08002BE10318}"}, 15 | {"testdata/inf03.txt", ""}, 16 | } 17 | 18 | for _, tc := range tcs { 19 | t.Run("", func(t *testing.T) { 20 | file, err := os.Open(tc.filename) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | actual := MatchClassGuid(file) 26 | if actual != tc.want { 27 | t.Fatal(actual, tc.want) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /managers/slackpkg.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | type slackpkg struct { 4 | common 5 | } 6 | 7 | func (m *slackpkg) load() error { 8 | m.commands = managerCommands{ 9 | install: "slackpkg", 10 | remove: "slackpkg", 11 | refresh: "slackpkg", 12 | update: "true", 13 | clean: "true", 14 | } 15 | 16 | m.flags = managerFlags{ 17 | global: []string{ 18 | "-batch=on", "-default_answer=y", 19 | }, 20 | install: []string{ 21 | "install", 22 | }, 23 | remove: []string{ 24 | "remove", 25 | }, 26 | refresh: []string{ 27 | "update", 28 | }, 29 | update: []string{ 30 | "upgrade-all", 31 | }, 32 | clean: []string{ 33 | "clean-system", 34 | }, 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /windows/testdata/inf02.txt: -------------------------------------------------------------------------------- 1 | ;/*++ 2 | ; 3 | ;Copyright (c) 2008-2023 Red Hat Inc. 4 | ; 5 | ; 6 | ;Module Name: 7 | ; viostor.inf 8 | ; 9 | ;Abstract: 10 | ; 11 | ;Installation Notes: 12 | ; Step by step driver installation wiki: 13 | ; https://github.com/virtio-win/kvm-guest-drivers-windows/wiki/Driver-installation 14 | ; 15 | ;--*/ 16 | 17 | [Version] 18 | Signature="$Windows NT$" 19 | Class=SCSIAdapter 20 | ;ClassGUID={4d36e97b-e325-11ce-bfc1-08002be10318} 21 | ClassGUID={4D36E97B-E325-11CE-BFC1-08002BE10318} 22 | Provider=%VENDOR% 23 | DriverVer = 02/25/2024,100.94.104.24800 24 | CatalogFile=viostor.cat 25 | DriverPackageType = PlugAndPlay 26 | DriverPackageDisplayName = %VioStorScsi.DeviceDesc% 27 | PnpLockdown=1 28 | -------------------------------------------------------------------------------- /windows/testdata/inf03.txt: -------------------------------------------------------------------------------- 1 | ;/*++ 2 | ; 3 | ;Copyright (c) 2008-2023 Red Hat Inc. 4 | ; 5 | ; 6 | ;Module Name: 7 | ; viostor.inf 8 | ; 9 | ;Abstract: 10 | ; 11 | ;Installation Notes: 12 | ; Step by step driver installation wiki: 13 | ; https://github.com/virtio-win/kvm-guest-drivers-windows/wiki/Driver-installation 14 | ; 15 | ;--*/ 16 | 17 | ;[Version] 18 | Signature="$Windows NT$" 19 | Class=SCSIAdapter 20 | ;ClassGUID={4d36e97b-e325-11ce-bfc1-08002be10318} 21 | ClassGUID={4D36E97B-E325-11CE-BFC1-08002BE10318} 22 | Provider=%VENDOR% 23 | DriverVer = 02/25/2024,100.94.104.24800 24 | CatalogFile=viostor.cat 25 | DriverPackageType = PlugAndPlay 26 | DriverPackageDisplayName = %VioStorScsi.DeviceDesc% 27 | PnpLockdown=1 28 | -------------------------------------------------------------------------------- /managers/xbps.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | type xbps struct { 4 | common 5 | } 6 | 7 | func (m *xbps) load() error { 8 | m.commands = managerCommands{ 9 | clean: "xbps-remove", 10 | install: "xbps-install", 11 | refresh: "xbps-install", 12 | remove: "xbps-remove", 13 | update: "sh", 14 | } 15 | 16 | m.flags = managerFlags{ 17 | global: []string{}, 18 | clean: []string{ 19 | "--yes", 20 | "--clean-cache", 21 | }, 22 | install: []string{ 23 | "--yes", 24 | }, 25 | refresh: []string{ 26 | "--sync", 27 | }, 28 | remove: []string{ 29 | "--yes", 30 | "--recursive", 31 | "--remove-orphans", 32 | }, 33 | update: []string{ 34 | "-c", 35 | "xbps-install --yes --update && xbps-install --yes --update", 36 | }, 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - errorlint 5 | - godot 6 | - misspell 7 | - whitespace 8 | exclusions: 9 | generated: lax 10 | presets: 11 | - comments 12 | - common-false-positives 13 | - legacy 14 | - std-error-handling 15 | paths: 16 | - third_party$ 17 | - builtin$ 18 | - examples$ 19 | rules: 20 | - linters: 21 | - staticcheck 22 | text: "ST1005:" 23 | formatters: 24 | enable: 25 | - gci 26 | - gofumpt 27 | settings: 28 | gci: 29 | sections: 30 | - standard 31 | - default 32 | - prefix(github.com/lxc/distrobuilder) 33 | exclusions: 34 | generated: lax 35 | paths: 36 | - third_party$ 37 | - builtin$ 38 | - examples$ 39 | -------------------------------------------------------------------------------- /managers/opkg.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type opkg struct { 8 | common 9 | } 10 | 11 | func (m *opkg) load() error { 12 | m.commands = managerCommands{ 13 | clean: "rm", 14 | install: "opkg", 15 | refresh: "opkg", 16 | remove: "opkg", 17 | update: "echo", 18 | } 19 | 20 | m.flags = managerFlags{ 21 | clean: []string{ 22 | "-rf", "/tmp/opkg-lists/", 23 | }, 24 | global: []string{}, 25 | install: []string{ 26 | "install", 27 | }, 28 | remove: []string{ 29 | "remove", 30 | }, 31 | refresh: []string{ 32 | "update", 33 | }, 34 | update: []string{ 35 | "Not supported", 36 | }, 37 | } 38 | 39 | m.hooks = managerHooks{ 40 | preRefresh: func() error { 41 | return os.MkdirAll("/var/lock", 0o755) 42 | }, 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /sources/openwrt-http_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestOpenWrtHTTP_getLatestServiceRelease(t *testing.T) { 12 | s := &openwrt{} 13 | s.client = http.DefaultClient 14 | 15 | tests := []struct { 16 | release string 17 | want *regexp.Regexp 18 | }{ 19 | { 20 | "22.03", 21 | regexp.MustCompile(`22\.03\.\d+`), 22 | }, 23 | { 24 | "23.05", 25 | regexp.MustCompile(`23\.05\.\d+`), 26 | }, 27 | { 28 | "24.10", 29 | regexp.MustCompile(`24\.10\.\d+`), 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | baseURL := "https://downloads.openwrt.org/releases/" 35 | release, err := s.getLatestServiceRelease(baseURL, tt.release) 36 | require.NoError(t, err) 37 | require.Regexp(t, tt.want, release) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.sphinx/.markdownlint/doc-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | if ! command -v mdl >/dev/null; then 4 | echo "Install mdl with 'snap install mdl' first." 5 | exit 1 6 | fi 7 | 8 | trap "rm -rf .tmp/" EXIT 9 | 10 | ## Preprocessing 11 | 12 | for fn in $(find doc/ -name '*.md'); do 13 | mkdir -p $(dirname ".tmp/$fn"); 14 | sed -E "s/(\(.+\)=)/\1\n/" $fn > .tmp/$fn; 15 | done 16 | 17 | mdl .tmp/doc -s.sphinx/.markdownlint/style.rb -u.sphinx/.markdownlint/rules.rb --ignore-front-matter > .tmp/errors.txt || true 18 | 19 | if [ ! -s ".tmp/errors.txt" ]; then 20 | echo "Passed!" 21 | exit 0 22 | fi 23 | 24 | ## Postprocessing 25 | 26 | filtered_errors="$(grep -vxFf .sphinx/.markdownlint/exceptions.txt .tmp/errors.txt)" 27 | if [ "$(echo "$filtered_errors" | wc -l)" = "2" ]; then 28 | echo "Passed!" 29 | exit 0 30 | else 31 | echo "Failed!" 32 | echo "$filtered_errors" 33 | exit 1 34 | fi 35 | -------------------------------------------------------------------------------- /managers/custom.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | type custom struct { 4 | common 5 | } 6 | 7 | func (m *custom) load() error { 8 | m.commands = managerCommands{ 9 | clean: m.definition.Packages.CustomManager.Clean.Command, 10 | install: m.definition.Packages.CustomManager.Install.Command, 11 | refresh: m.definition.Packages.CustomManager.Refresh.Command, 12 | remove: m.definition.Packages.CustomManager.Remove.Command, 13 | update: m.definition.Packages.CustomManager.Update.Command, 14 | } 15 | 16 | m.flags = managerFlags{ 17 | clean: m.definition.Packages.CustomManager.Clean.Flags, 18 | install: m.definition.Packages.CustomManager.Install.Flags, 19 | refresh: m.definition.Packages.CustomManager.Refresh.Flags, 20 | remove: m.definition.Packages.CustomManager.Remove.Flags, 21 | update: m.definition.Packages.CustomManager.Update.Flags, 22 | global: m.definition.Packages.CustomManager.Flags, 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /distrobuilder/main_validate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type cmdValidate struct { 10 | cmdValidate *cobra.Command 11 | global *cmdGlobal 12 | } 13 | 14 | func (c *cmdValidate) command() *cobra.Command { 15 | c.cmdValidate = &cobra.Command{ 16 | Use: "validate ", 17 | Short: "Validate definition file", 18 | Args: cobra.ExactArgs(1), 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | // Get the image definition 21 | _, err := getDefinition(args[0], c.global.flagOptions) 22 | if err != nil { 23 | return fmt.Errorf("Failed to get definition: %w", err) 24 | } 25 | 26 | return nil 27 | }, 28 | SilenceUsage: true, 29 | SilenceErrors: true, 30 | } 31 | 32 | c.cmdValidate.Flags().StringSliceVarP(&c.global.flagOptions, "options", "o", 33 | []string{}, "Override options (list of key=value)"+"``") 34 | 35 | return c.cmdValidate 36 | } 37 | -------------------------------------------------------------------------------- /managers/dnf.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "github.com/lxc/distrobuilder/shared" 5 | ) 6 | 7 | type dnf struct { 8 | common 9 | } 10 | 11 | // NewDnf creates a new Manager instance. 12 | func (m *dnf) load() error { 13 | m.commands = managerCommands{ 14 | clean: "dnf", 15 | install: "dnf", 16 | refresh: "dnf", 17 | remove: "dnf", 18 | update: "dnf", 19 | } 20 | 21 | m.flags = managerFlags{ 22 | global: []string{ 23 | "-y", 24 | }, 25 | install: []string{ 26 | "install", 27 | "--nobest", 28 | }, 29 | remove: []string{ 30 | "remove", 31 | }, 32 | refresh: []string{ 33 | "makecache", 34 | }, 35 | update: []string{ 36 | "upgrade", 37 | "--nobest", 38 | }, 39 | clean: []string{ 40 | "clean", "all", 41 | }, 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (m *dnf) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 48 | return yumManageRepository(repoAction) 49 | } 50 | -------------------------------------------------------------------------------- /testdata/testfile.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNED MESSAGE----- 2 | Hash: SHA512 3 | 4 | I need to be verified. 5 | -----BEGIN PGP SIGNATURE----- 6 | 7 | iQIzBAEBCgAdFiEEYfHyDZhnHyXKGYh8mTQI0RN7fVEFAlqFpu8ACgkQmTQI0RN7 8 | fVGj3w/7BCzAkG995rA/7ba371SW/5uifLKxEn/izWzuJsEO40BN0rzV53XsIqew 9 | TMhudZo2r1lF7L0KkVChCl/E//aGB5srHRmQlogJqjdyw4qCuVmTe/QMadjo67fS 10 | wSqH40p5KCQeLZ33xF60vbMwf7ZwtSesFnCsQyvhu85+FDpuexGKKKDxSmO4WjHV 11 | lL5nDZ0vtSghw3yobGWiYBQ/6MqGLkL6yK0LAY50slywTgAb5WtSE2YCTTLeJOEi 12 | PEWMWbWoRYmN9ijUowo9YP6cKj4Fz0LtbWMBuHDgvO7Zl/qrb57NxRgBM0cCzAnR 13 | zEwjRjcfK7GGk+NyfAGbeabgJT/ATI/51sB3MBJgbd+FcSt4zMUL2qfwFDtrTqK3 14 | 7NaKOUh7fVnsGeKY/4DSz0+hJy4qR9JuawDCuiS8CJHzp9LKxKmQDhFfpmFYWOOr 15 | Nqc4PifAc0OQ3n1iJGMZ0I5CSP79hRLu7FTyOEhARAz1VMR9lOmEAT+M7RcbENs6 16 | U06mI5h5tyKyBt0cUKQSKtYGKydR2+ZGVkkjEpodcU9RpRzvBFQMU23XdtVPNnya 17 | sf3ddNIbkaWkF17oxy7PW4ZFnWbA8wATEnnWi3dPIGhRRdS2qJioXFziW/idSkUB 18 | AagCicMVQ1XDX/Hg5HrwUBrGBk1JZ3TTwzZ/kpePgry1XSLuGxI= 19 | =vSiP 20 | -----END PGP SIGNATURE----- 21 | -------------------------------------------------------------------------------- /testdata/testfile-invalid.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNED MESSAGE----- 2 | Hash: SHA512 3 | 4 | I need to be verified. 5 | -----BEGIN PGP SIGNATURE----- 6 | 7 | kQIzBAEBCgAdFiEEYfHyDZhnHyXKGYh8mTQI0RN7fVEFAlqFpu8ACgkQmTQI0RN7 8 | fVGj3w/7BCzAkG995rA/7ba371SW/5uifLKxEn/izWzuJsEO40BN0rzV53XsIqew 9 | TMhudZo2r1lF7L0KkVChCl/E//aGB5srHRmQlogJqjdyw4qCuVmTe/QMadjo67fS 10 | wSqH40p5KCQeLZ33xF60vbMwf7ZwtSesFnCsQyvhu85+FDpuexGKKKDxSmO4WjHV 11 | lL5nDZ0vtSghw3yobGWiYBQ/6MqGLkL6yK0LAY50slywTgAb5WtSE2YCTTLeJOEi 12 | PEWMWbWoRYmN9ijUowo9YP6cKj4Fz0LtbWMBuHDgvO7Zl/qrb57NxRgBM0cCzAnR 13 | zEwjRjcfK7GGk+NyfAGbeabgJT/ATI/51sB3MBJgbd+FcSt4zMUL2qfwFDtrTqK3 14 | 7NaKOUh7fVnsGeKY/4DSz0+hJy4qR9JuawDCuiS8CJHzp9LKxKmQDhFfpmFYWOOr 15 | Nqc4PifAc0OQ3n1iJGMZ0I5CSP79hRLu7FTyOEhARAz1VMR9lOmEAT+M7RcbENs6 16 | U06mI5h5tyKyBt0cUKQSKtYGKydR2+ZGVkkjEpodcU9RpRzvBFQMU23XdtVPNnya 17 | sf3ddNIbkaWkF17oxy7PW4ZFnWbA8wATEnnWi3dPIGhRRdS2qJioXFziW/idSkUB 18 | AagCicMVQ1XDX/Hg5HrwUBrGBk1JZ3TTwzZ/kpePgry1XSLuGxI= 19 | =vSiP 20 | -----END PGP SIGNATURE----- 21 | -------------------------------------------------------------------------------- /generators/common.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/lxc/distrobuilder/shared" 7 | ) 8 | 9 | type common struct { 10 | logger *logrus.Logger 11 | cacheDir string 12 | sourceDir string 13 | defFile shared.DefinitionFile 14 | } 15 | 16 | func (g *common) init(logger *logrus.Logger, cacheDir string, sourceDir string, defFile shared.DefinitionFile, def shared.Definition) { 17 | g.logger = logger 18 | g.cacheDir = cacheDir 19 | g.sourceDir = sourceDir 20 | g.defFile = defFile 21 | 22 | render := func(val string) string { 23 | if !defFile.Pongo { 24 | return val 25 | } 26 | 27 | out, err := shared.RenderTemplate(val, def) 28 | if err != nil { 29 | logger.WithField("err", err).Warn("Failed to render template") 30 | return val 31 | } 32 | 33 | return out 34 | } 35 | 36 | if defFile.Pongo { 37 | g.defFile.Content = render(g.defFile.Content) 38 | g.defFile.Path = render(g.defFile.Path) 39 | g.defFile.Source = render(g.defFile.Source) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shared/osarch_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetArch(t *testing.T) { 11 | tests := []struct { 12 | distro string 13 | arch string 14 | expected string 15 | }{ 16 | { 17 | "alpinelinux", 18 | "x86_64", 19 | "x86_64", 20 | }, 21 | { 22 | "centos", 23 | "x86_64", 24 | "x86_64", 25 | }, 26 | { 27 | "debian", 28 | "amd64", 29 | "amd64", 30 | }, 31 | { 32 | "debian", 33 | "x86_64", 34 | "amd64", 35 | }, 36 | { 37 | "debian", 38 | "s390x", 39 | "s390x", 40 | }, 41 | } 42 | 43 | for i, tt := range tests { 44 | log.Printf("Running test #%d: %s %s", i, tt.distro, tt.arch) 45 | arch, err := GetArch(tt.distro, tt.arch) 46 | require.NoError(t, err) 47 | require.Equal(t, tt.expected, arch) 48 | } 49 | 50 | _, err := GetArch("distro", "") 51 | require.EqualError(t, err, "Architecture map isn't supported: distro") 52 | 53 | _, err = GetArch("debian", "arch") 54 | require.EqualError(t, err, "Architecture isn't supported: arch") 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/commits.yml: -------------------------------------------------------------------------------- 1 | name: Commits 2 | on: 3 | - pull_request 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dco-check: 10 | permissions: 11 | pull-requests: read # for tim-actions/get-pr-commits to get list of commits from the PR 12 | name: Signed-off-by (DCO) 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Get PR Commits 16 | id: 'get-pr-commits' 17 | uses: tim-actions/get-pr-commits@master 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Check that all commits are signed-off 22 | uses: tim-actions/dco@master 23 | with: 24 | commits: ${{ steps.get-pr-commits.outputs.commits }} 25 | 26 | target-branch: 27 | permissions: 28 | contents: none 29 | name: Branch target 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Check branch target 33 | env: 34 | TARGET: ${{ github.event.pull_request.base.ref }} 35 | run: | 36 | set -x 37 | [ "${TARGET}" = "main" ] && exit 0 38 | 39 | echo "Invalid branch target: ${TARGET}" 40 | exit 1 41 | -------------------------------------------------------------------------------- /sources/nixos-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/lxc/distrobuilder/shared" 8 | ) 9 | 10 | type nixos struct { 11 | common 12 | } 13 | 14 | func (s *nixos) Run() error { 15 | hydraProject := "nixos" 16 | 17 | hydraJobset := fmt.Sprintf("release-%s", s.definition.Image.Release) 18 | releaseAttr := "incusContainerImage" 19 | hydraBuildProduct := "squashfs-image" 20 | 21 | if s.definition.Image.Release == "unstable" { 22 | hydraJobset = "trunk-combined" 23 | } 24 | 25 | hydraJob := fmt.Sprintf("nixos.%s.%s-linux", releaseAttr, s.definition.Image.ArchitectureMapped) 26 | 27 | imageURL := fmt.Sprintf("https://hydra.nixos.org/job/%s/%s/%s/latest/download-by-type/file/%s", hydraProject, hydraJobset, hydraJob, hydraBuildProduct) 28 | 29 | fpath, err := s.DownloadHash(s.definition.Image, imageURL, "", nil) 30 | if err != nil { 31 | return fmt.Errorf("Failed downloading rootfs: %w", err) 32 | } 33 | 34 | err = shared.Unpack(filepath.Join(fpath, hydraBuildProduct), s.rootfsDir) 35 | if err != nil { 36 | return fmt.Errorf("Failed unpacking rootfs: %w", err) 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /doc/howto/troubleshoot.md: -------------------------------------------------------------------------------- 1 | # Troubleshoot `distrobuilder` 2 | 3 | This section covers some of the most commonly encountered problems and gives instructions for resolving them. 4 | 5 | ## Cannot install into target 6 | 7 | > Error `Cannot install into target '/var/cache/distrobuilder.123456789/rootfs' mounted with noexec or nodev` 8 | 9 | You have installed `distrobuilder` into an Incus container and you are trying to run it. `distrobuilder` does not run in an Incus container. Run `distrobuilder` on the host, or in a VM. 10 | 11 | ## Classic confinement 12 | 13 | > Error `error: This revision of snap "distrobuilder" was published using classic confinement` 14 | 15 | You are trying to install the `distrobuilder` snap package. The `distrobuilder` snap package has been configured to use the `classic` confinement. Therefore, when you install it, you have to add the flag `--classic` as shown above in the instructions. 16 | 17 | ## Must be root 18 | 19 | > Error `You must be root to run this tool` 20 | 21 | You must be _root_ in order to run the `distrobuilder` tool. The tool runs commands such as `mknod` that require administrative privileges. Use `sudo` when running `distrobuilder`. 22 | -------------------------------------------------------------------------------- /test/lint/newline-after-block.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | echo "Checking that functional blocks are followed by newlines..." 4 | 5 | # Check all .go files except the protobuf bindings (.pb.go) 6 | files=$(git ls-files --cached --modified --others '*.go' ':!:*.pb.go') 7 | 8 | exit_code=0 9 | for file in $files 10 | do 11 | # This oneliner has a few steps: 12 | # 1. sed: 13 | # a. Check for lines that contain a single closing brace (plus whitespace). 14 | # b. Move the pattern space window forward to the next line. 15 | # c. Match lines that start with a word character. This allows for a closing brace on subsequent lines. 16 | # d. Print the line number. 17 | # 2. xargs: Print the filename next to the line number of the matches (piped). 18 | # 3. If there were no matches, the file name without the line number is printed, use grep to filter it out. 19 | # 4. Replace the space with a colon to make a clickable link. 20 | RESULT=$(sed -n -e '/^\s*}\s*$/{n;/^\s*\w/{;=}}' "$file" | xargs -L 1 echo "$file" | grep -v '\.go$' | sed 's/ /:/g') 21 | if [ -n "${RESULT}" ]; then 22 | echo "${RESULT}" 23 | exit_code=1 24 | fi 25 | done 26 | 27 | exit $exit_code 28 | -------------------------------------------------------------------------------- /doc/reference/mappings.md: -------------------------------------------------------------------------------- 1 | # Mappings 2 | 3 | `mappings` describes an architecture mapping between the architectures from those used in Incus and those used by the distribution. 4 | These mappings are useful if you for example want to build a `x86_64` image but the source tarball contains `amd64` as its architecture. 5 | 6 | ```yaml 7 | mappings: 8 | architectures: 9 | architecture_map: 10 | ``` 11 | 12 | It's possible to specify a custom map using the `architectures` field. 13 | Here's an example of a custom mapping: 14 | 15 | ```yaml 16 | mappings: 17 | architectures: 18 | i686: i386 19 | x86_64: amd64 20 | armv7l: armhf 21 | aarch64: arm64 22 | ppc: powerpc 23 | ppc64: powerpc64 24 | ppc64le: ppc64el 25 | ``` 26 | 27 | The mapped architecture can be accessed via `Image.ArchitectureMapped` in the code or `image.architecture_mapped` in the definition file. 28 | 29 | There are some preset mappings which can be used in the `architecture_map` field. 30 | Those are: 31 | 32 | * `alpinelinux` 33 | * `altlinux` 34 | * `archlinux` 35 | * `centos` 36 | * `debian` 37 | * `funtoo` 38 | * `gentoo` 39 | * `plamolinux` 40 | * `voidlinux` 41 | -------------------------------------------------------------------------------- /sources/rootfs-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path" 7 | "path/filepath" 8 | 9 | "github.com/lxc/distrobuilder/shared" 10 | ) 11 | 12 | type rootfs struct { 13 | common 14 | } 15 | 16 | // Run downloads a tarball. 17 | func (s *rootfs) Run() error { 18 | URL, err := url.Parse(s.definition.Source.URL) 19 | if err != nil { 20 | return fmt.Errorf("Failed to parse URL: %w", err) 21 | } 22 | 23 | var fpath string 24 | var filename string 25 | 26 | if URL.Scheme == "file" { 27 | fpath = filepath.Dir(URL.Path) 28 | filename = filepath.Base(URL.Path) 29 | } else { 30 | fpath, err = s.DownloadHash(s.definition.Image, s.definition.Source.URL, "", nil) 31 | if err != nil { 32 | return fmt.Errorf("Failed to download %q: %w", s. 33 | definition.Source.URL, err) 34 | } 35 | 36 | filename = path.Base(s.definition.Source.URL) 37 | } 38 | 39 | s.logger.WithField("file", filepath.Join(fpath, filename)).Info("Unpacking image") 40 | 41 | // Unpack 42 | err = shared.Unpack(filepath.Join(fpath, filename), s.rootfsDir) 43 | if err != nil { 44 | return fmt.Errorf("Failed to unpack %q: %w", filepath.Join(fpath, filename), err) 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /doc/reference/image.md: -------------------------------------------------------------------------------- 1 | # Image 2 | 3 | The image section describes the image output. 4 | 5 | ```yaml 6 | image: 7 | distribution: # required 8 | architecture: 9 | description: 10 | expiry: 11 | name: 12 | release: 13 | serial: 14 | variant: 15 | ``` 16 | 17 | The fields `distribution`, `architecture`, `description` and `release` are self-explanatory. 18 | If `architecture` is not set, it defaults to the host's architecture. 19 | 20 | The `expiry` field describes the image expiry. 21 | The format is `\d+(s|m|h|d|w)` (seconds, minutes, hours, days, weeks), and defaults to 30 days (`30d`). 22 | It's also possible to define multiple such parts, e.g. `1h 30m 10s`. 23 | 24 | The `name` field is used in the Incus metadata as well as the output name for Incus unified tarballs. 25 | It defaults to `{{ image.distribution }}-{{ image.release }}-{{ image.architecture_mapped }}-{{ image.variant }}-{{ image.serial }}`. 26 | 27 | The `serial` field is the image's serial number. 28 | It can be anything and defaults to `YYYYmmdd_HHMM` (date format). 29 | 30 | The `variant` field can be anything and is used in the Incus metadata as well as for [filtering](filters.md). 31 | -------------------------------------------------------------------------------- /generators/utils.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | func updateFileAccess(file *os.File, defFile shared.DefinitionFile) error { 12 | // Change file mode if needed 13 | if defFile.Mode != "" { 14 | mode, err := strconv.ParseUint(defFile.Mode, 8, 64) 15 | if err != nil { 16 | return fmt.Errorf("Failed to parse file mode: %w", err) 17 | } 18 | 19 | err = file.Chmod(os.FileMode(mode)) 20 | if err != nil { 21 | return fmt.Errorf("Failed to change file mode: %w", err) 22 | } 23 | } 24 | 25 | // Change gid if needed 26 | if defFile.GID != "" { 27 | gid, err := strconv.Atoi(defFile.GID) 28 | if err != nil { 29 | return fmt.Errorf("Failed to parse GID: %w", err) 30 | } 31 | 32 | err = file.Chown(-1, gid) 33 | if err != nil { 34 | return fmt.Errorf("Failed to change GID: %w", err) 35 | } 36 | } 37 | 38 | // Change uid if needed 39 | if defFile.UID != "" { 40 | uid, err := strconv.Atoi(defFile.UID) 41 | if err != nil { 42 | return fmt.Errorf("Failed to parse UID: %w", err) 43 | } 44 | 45 | err = file.Chown(uid, -1) 46 | if err != nil { 47 | return fmt.Errorf("Failed to change UID: %w", err) 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /doc/reference/targets.md: -------------------------------------------------------------------------------- 1 | # Targets 2 | 3 | The target section is for target dependent files. 4 | 5 | ```yaml 6 | targets: 7 | lxc: 8 | create_message: 9 | config: 10 | - type: 11 | before: 12 | after: 13 | content: 14 | - ... 15 | incus: 16 | vm: 17 | size: 18 | filesystem: 19 | ``` 20 | 21 | ## LXC 22 | 23 | The `create_message` field is a string which is displayed after new LXC container has been created. 24 | This string is rendered using Pongo2 and can include various fields from the definition file, e.g. `{{ image.description }}`. 25 | 26 | `config` is a list of container configuration options. 27 | The `type` must be `all`, `system` or `user`. 28 | 29 | The keys `before` and `after` are used for compatibility. 30 | Currently, the maximum value for compatibility is 5. 31 | If your desired compatibility level is 3 for example, you would use `before: 4` and `after: 2`. 32 | 33 | `content` describes the configuration which is to be written to the configuration file. 34 | 35 | ## Incus 36 | 37 | Valid keys are `size` and `filesystem`. 38 | The former specifies the VM image size in bytes. 39 | The latter specifies the root partition file system. 40 | It currently supports `ext4` and `btrfs`. 41 | -------------------------------------------------------------------------------- /windows/testdata/winpe_boot_wim_info.txt: -------------------------------------------------------------------------------- 1 | WIM Information: 2 | ---------------- 3 | Path: winpe/amd64/media/sources/boot.wim 4 | GUID: 0x36f532ff377d1545a1c46bb542ec2d33 5 | Version: 68864 6 | Image Count: 1 7 | Compression: LZX 8 | Chunk Size: 32768 bytes 9 | Part Number: 1/1 10 | Boot Index: 1 11 | Size: 479260185 bytes 12 | Attributes: Relative path junction 13 | 14 | Available Images: 15 | ----------------- 16 | Index: 1 17 | Name: Microsoft Windows PE (amd64) 18 | Description: Microsoft Windows PE (amd64) 19 | Directory Count: 5223 20 | File Count: 21703 21 | Total Bytes: 2894943499 22 | Hard Link Bytes: 1412981622 23 | Creation Time: Sun Jun 11 03:56:01 2023 UTC 24 | Last Modification Time: Mon Apr 08 06:26:00 2024 UTC 25 | Architecture: x86_64 26 | Product Name: Microsoft® Windows® Operating System 27 | Edition ID: WindowsPE 28 | Installation Type: WindowsPE 29 | Product Type: WinNT 30 | Languages: en-US 31 | Default Language: en-US 32 | System Root: WINDOWS 33 | Major Version: 10 34 | Minor Version: 0 35 | Build: 25398 36 | Service Pack Build: 1 37 | Service Pack Level: 0 38 | WIMBoot compatible: no 39 | -------------------------------------------------------------------------------- /managers/zypper.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lxc/distrobuilder/shared" 7 | ) 8 | 9 | type zypper struct { 10 | common 11 | } 12 | 13 | func (m *zypper) load() error { 14 | m.commands = managerCommands{ 15 | clean: "zypper", 16 | install: "zypper", 17 | refresh: "zypper", 18 | remove: "zypper", 19 | update: "zypper", 20 | } 21 | 22 | m.flags = managerFlags{ 23 | global: []string{ 24 | "--non-interactive", 25 | "--gpg-auto-import-keys", 26 | }, 27 | clean: []string{ 28 | "clean", 29 | "-a", 30 | }, 31 | install: []string{ 32 | "install", 33 | "--allow-downgrade", 34 | "--replacefiles", 35 | }, 36 | remove: []string{ 37 | "remove", 38 | }, 39 | refresh: []string{ 40 | "refresh", 41 | }, 42 | update: []string{ 43 | "update", 44 | }, 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (m *zypper) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 51 | if repoAction.Type != "" && repoAction.Type != "zypper" { 52 | return errors.New("Invalid repository Type") 53 | } 54 | 55 | if repoAction.Name == "" { 56 | return errors.New("Invalid repository name") 57 | } 58 | 59 | if repoAction.URL == "" { 60 | return errors.New("Invalid repository url") 61 | } 62 | 63 | return shared.RunCommand(m.ctx, nil, nil, "zypper", "ar", "--refresh", "--check", repoAction.URL, repoAction.Name) 64 | } 65 | -------------------------------------------------------------------------------- /generators/generators_test.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | func setup(t *testing.T, cacheDir string) { 16 | // Create rootfs directory 17 | err := os.MkdirAll(filepath.Join(cacheDir, "rootfs"), 0o755) 18 | require.NoError(t, err) 19 | } 20 | 21 | func teardown(cacheDir string) { 22 | os.RemoveAll(cacheDir) 23 | } 24 | 25 | func TestGet(t *testing.T) { 26 | generator, err := Load("hostname", nil, "", "", shared.DefinitionFile{}, shared.Definition{}) 27 | require.IsType(t, &hostname{}, generator) 28 | require.NoError(t, err) 29 | 30 | generator, err = Load("", nil, "", "", shared.DefinitionFile{}, shared.Definition{}) 31 | require.Nil(t, generator) 32 | require.Error(t, err) 33 | } 34 | 35 | func createTestFile(t *testing.T, path, content string) { 36 | file, err := os.Create(path) 37 | require.NoError(t, err) 38 | defer file.Close() 39 | 40 | _, err = file.WriteString(content) 41 | require.NoError(t, err) 42 | } 43 | 44 | func validateTestFile(t *testing.T, path, content string) { 45 | file, err := os.Open(path) 46 | require.NoError(t, err) 47 | defer file.Close() 48 | 49 | var buffer bytes.Buffer 50 | _, err = io.Copy(&buffer, file) 51 | require.NoError(t, err) 52 | 53 | require.Equal(t, content, buffer.String()) 54 | } 55 | -------------------------------------------------------------------------------- /windows/inf.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | versionRe = regexp.MustCompile(`(?i)^\[Version\][ ]*$`) 14 | classGuidRe = regexp.MustCompile(`(?i)^ClassGuid[ ]*=[ ]*(.+)$`) 15 | ) 16 | 17 | func ParseDriverClassGuid(driverName, infPath string) (string, error) { 18 | // Retrieve the ClassGuid which is needed for the Windows registry entries. 19 | file, err := os.Open(infPath) 20 | if err != nil { 21 | err = fmt.Errorf("Failed to open driver %s inf %s: %w", driverName, infPath, err) 22 | return "", err 23 | } 24 | 25 | defer func() { _ = file.Close() }() 26 | 27 | classGuid := MatchClassGuid(file) 28 | if classGuid == "" { 29 | return "", fmt.Errorf("Failed to parse driver %s classGuid %s", driverName, infPath) 30 | } 31 | 32 | return classGuid, nil 33 | } 34 | 35 | func MatchClassGuid(r io.Reader) (classGuid string) { 36 | scanner := bufio.NewScanner(r) 37 | versionFlag := false 38 | for scanner.Scan() { 39 | line := scanner.Text() 40 | if !versionFlag { 41 | if versionRe.MatchString(line) { 42 | versionFlag = true 43 | } 44 | 45 | continue 46 | } 47 | 48 | matches := classGuidRe.FindStringSubmatch(line) 49 | if len(matches) > 1 { 50 | classGuid = strings.TrimSpace(matches[1]) 51 | if classGuid != "" { 52 | return classGuid 53 | } 54 | } 55 | } 56 | 57 | return classGuid 58 | } 59 | -------------------------------------------------------------------------------- /doc/reference/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | ```yaml 4 | actions: 5 | - trigger: # required 6 | action: |- 7 | #!/bin/bash 8 | echo "Run me" 9 | architectures: # filter 10 | releases: # filter 11 | variants: # filter 12 | ``` 13 | 14 | Actions are scripts that are to be run after certain steps during the building process. 15 | Each action has two fields, `trigger` and `action`, as well as some filters. 16 | The `trigger` field describes the step after which the `action` is to be run. 17 | Valid triggers are: 18 | 19 | * `post-unpack` 20 | * `post-update` 21 | * `post-packages` 22 | * `post-files` 23 | 24 | The above list also shows the order in which the actions are processed. 25 | 26 | After the root file system has been unpacked, all `post-unpack` actions are run. 27 | 28 | After the package manager has updated all packages, (given that `packages.update` is `true`), all `post-update` actions are run. 29 | After the package manager has installed the requested packages, all `post-packages` actions are run. 30 | For more on `packages`, see [packages](packages.md). 31 | 32 | And last, after the `files` section has been processed, all `post-files` actions are run. 33 | This action runs only for `build-lxc`, `build-incus`, `pack-lxc`, and `pack-incus`. 34 | You can also force enable post-files for `build-dir` with option `--with-post-files` 35 | For more on `files`, see [generators](generators.md). 36 | -------------------------------------------------------------------------------- /generators/fstab.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/lxc/distrobuilder/image" 10 | "github.com/lxc/distrobuilder/shared" 11 | ) 12 | 13 | type fstab struct { 14 | common 15 | } 16 | 17 | // RunLXC doesn't support the fstab generator. 18 | func (g *fstab) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error { 19 | return errors.New("fstab generator not supported for LXC") 20 | } 21 | 22 | // RunIncus writes to /etc/fstab. 23 | func (g *fstab) RunIncus(img *image.IncusImage, target shared.DefinitionTargetIncus) error { 24 | f, err := os.Create(filepath.Join(g.sourceDir, "etc/fstab")) 25 | if err != nil { 26 | return fmt.Errorf("Failed to create file %q: %w", filepath.Join(g.sourceDir, "etc/fstab"), err) 27 | } 28 | 29 | defer f.Close() 30 | 31 | content := `LABEL=rootfs / %s %s 0 0 32 | LABEL=UEFI /boot/efi vfat defaults 0 0 33 | ` 34 | 35 | fs := target.VM.Filesystem 36 | 37 | if fs == "" { 38 | fs = "ext4" 39 | } 40 | 41 | options := "defaults" 42 | 43 | if fs == "btrfs" { 44 | options = fmt.Sprintf("%s,subvol=@", options) 45 | } 46 | 47 | _, err = fmt.Fprintf(f, content, fs, options) 48 | if err != nil { 49 | return fmt.Errorf("Failed to write string to file %q: %w", filepath.Join(g.sourceDir, "etc/fstab"), err) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // Run does nothing. 56 | func (g *fstab) Run() error { 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /.sphinx/.markdownlint/rules.rb: -------------------------------------------------------------------------------- 1 | rule 'Myst-MD031', 'Fenced code blocks should be surrounded by blank lines' do 2 | tags :code, :blank_lines 3 | aliases 'blanks-around-fences' 4 | check do |doc| 5 | errors = [] 6 | # Some parsers (including kramdown) have trouble detecting fenced code 7 | # blocks without surrounding whitespace, so examine the lines directly. 8 | in_code = false 9 | fence = nil 10 | lines = [''] + doc.lines + [''] 11 | lines.each_with_index do |line, linenum| 12 | line.strip.match(/^(`{3,}|~{3,})/) 13 | unless Regexp.last_match(1) && 14 | ( 15 | !in_code || 16 | (Regexp.last_match(1).slice(0, fence.length) == fence) 17 | ) 18 | next 19 | end 20 | 21 | fence = in_code ? nil : Regexp.last_match(1) 22 | in_code = !in_code 23 | if (in_code && !(lines[linenum - 1].empty? || lines[linenum - 1].match(/^[:\-\*]*\s*\% /))) || 24 | (!in_code && !(lines[linenum + 1].empty? || lines[linenum + 1].match(/^\s*:/))) 25 | errors << linenum 26 | end 27 | end 28 | errors 29 | end 30 | end 31 | 32 | 33 | rule 'Myst-IDs', 'MyST IDs should be preceded by a blank line' do 34 | check do |doc| 35 | errors = [] 36 | ids = doc.matching_text_element_lines(/^\(.+\)=\s*$/) 37 | ids.each do |linenum| 38 | if (linenum > 1) && !doc.lines[linenum - 2].empty? 39 | errors << linenum 40 | end 41 | end 42 | errors.sort 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.sphinx/_templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "furo/page.html" %} 2 | 3 | {% block footer %} 4 | {% include "footer.html" %} 5 | {% endblock footer %} 6 | 7 | {% block body -%} 8 | {% include "header.html" %} 9 | {{ super() }} 10 | {%- endblock body %} 11 | 12 | {% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} 13 | {% set furo_hide_toc_orig = furo_hide_toc %} 14 | {% set furo_hide_toc=false %} 15 | {% endif %} 16 | 17 | {% block right_sidebar %} 18 |
19 | {% if not furo_hide_toc_orig %} 20 |
21 | 22 | {{ _("Contents") }} 23 | 24 |
25 |
26 |
27 | {{ toc }} 28 |
29 |
30 | {% endif %} 31 | {% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} 32 | 37 | 47 | {% endif %} 48 |
49 | {% endblock right_sidebar %} 50 | -------------------------------------------------------------------------------- /windows/testdata/w8_install_wim_info.txt: -------------------------------------------------------------------------------- 1 | WIM Information: 2 | ---------------- 3 | Path: install.wim 4 | GUID: 0xe46cb79f5de1f7458c96f3c1ce71d19f 5 | Version: 68864 6 | Image Count: 1 7 | Compression: LZX 8 | Chunk Size: 32768 bytes 9 | Part Number: 1/1 10 | Boot Index: 0 11 | Size: 2920155755 bytes 12 | Attributes: Integrity info, Relative path junction 13 | 14 | Available Images: 15 | ----------------- 16 | Index: 1 17 | Name: Windows 8 Enterprise 18 | Description: Windows 8 Enterprise 19 | Display Name: Windows 8 企业版 20 | Display Description: Windows 8 企业版 21 | Directory Count: 18457 22 | File Count: 83427 23 | Total Bytes: 12454110871 24 | Hard Link Bytes: 4972983529 25 | Creation Time: Thu Jul 26 11:57:02 2012 UTC 26 | Last Modification Time: Thu Jul 26 11:58:17 2012 UTC 27 | Architecture: x86_64 28 | Product Name: Microsoft® Windows® Operating System 29 | Edition ID: Enterprise 30 | Installation Type: Client 31 | HAL: acpiapic 32 | Product Type: WinNT 33 | Product Suite: Terminal Server 34 | Languages: zh-CN 35 | Default Language: zh-CN 36 | System Root: WINDOWS 37 | Major Version: 6 38 | Minor Version: 2 39 | Build: 9200 40 | Service Pack Build: 16384 41 | Service Pack Level: 0 42 | Flags: Enterprise 43 | WIMBoot compatible: no 44 | -------------------------------------------------------------------------------- /distrobuilder/passwd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // parsePasswdAndGroupFiles reads passwd and group files from the given root directory 12 | // and returns mappings of names to IDs for both users and groups. 13 | func parsePasswdAndGroupFiles(rootDir string) (map[string]string, map[string]string, error) { 14 | userMap, err := parsePasswdFile(filepath.Join(rootDir, "/etc/passwd")) 15 | if err != nil { 16 | return nil, nil, fmt.Errorf("parsing passwd file: %w", err) 17 | } 18 | 19 | groupMap, err := parsePasswdFile(filepath.Join(rootDir, "/etc/group")) 20 | if err != nil { 21 | return nil, nil, fmt.Errorf("parsing group file: %w", err) 22 | } 23 | 24 | return userMap, groupMap, nil 25 | } 26 | 27 | // parsePasswdFile reads a passwd-format file and returns a map of names to IDs. 28 | func parsePasswdFile(path string) (map[string]string, error) { 29 | idMap := make(map[string]string) 30 | 31 | data, err := os.ReadFile(path) 32 | if err != nil { 33 | return nil, fmt.Errorf("reading file %s: %w", path, err) 34 | } 35 | 36 | for _, line := range strings.Split(string(data), "\n") { 37 | if line == "" { 38 | continue 39 | } 40 | 41 | fields := strings.Split(line, ":") 42 | if len(fields) < 3 { 43 | continue 44 | } 45 | 46 | idMap[fields[0]] = fields[2] 47 | } 48 | 49 | return idMap, nil 50 | } 51 | 52 | // isNumeric is a helper function to check if a string is numeric. 53 | func isNumeric(s string) bool { 54 | _, err := strconv.Atoi(s) 55 | return err == nil 56 | } 57 | -------------------------------------------------------------------------------- /windows/driver_netkvm.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | var driverNetKVM = DriverInfo{ 4 | PackageName: "netkvm.inf_amd64_805ee20efb26a964", 5 | DriversRegistry: `[\DriverDatabase\DeviceIds\pci\VEN_1AF4&DEV_1000] 6 | "{{ infFile }}"=hex(3):02,ff,00,00 7 | 8 | [\DriverDatabase\DeviceIds\pci\VEN_1AF4&DEV_1000&SUBSYS_00011AF4&REV_00] 9 | "{{ infFile }}"=hex(3):01,ff,00,00 10 | 11 | [\DriverDatabase\DeviceIds\pci\VEN_1AF4&DEV_1041] 12 | "{{ infFile }}"=hex(3):02,ff,00,00 13 | 14 | [\DriverDatabase\DeviceIds\pci\VEN_1AF4&DEV_1041&SUBSYS_11001AF4&REV_01] 15 | "{{ infFile }}"=hex(3):01,ff,00,00 16 | 17 | [\DriverDatabase\DeviceIds\{{ classGuid|lower }}] 18 | "{{ infFile }}"=hex(0): 19 | 20 | [\DriverDatabase\DriverInfFiles\{{ infFile }}] 21 | @=hex(7):{{ packageName|toHex }},00,00,00,00 22 | "Active"=hex(1):{{ packageName|toHex }},00,00 23 | 24 | [\DriverDatabase\DriverPackages\{{ packageName }}] 25 | @=hex(1):{{ infFile|toHex }},00,00 26 | "Catalog"=hex(1):6e,00,65,00,74,00,6b,00,76,00,6d,00,2e,00,63,00,61,00,74,00,00,00 27 | "ImportDate"=hex(3):40,8f,c3,dd,bd,e9,d6,01 28 | "InfName"=hex(1):6e,00,65,00,74,00,6b,00,76,00,6d,00,2e,00,69,00,6e,00,66,00,00,00 29 | "OemPath"=hex(1):45,00,3a,00,5c,00,4e,00,65,00,74,00,4b,00,56,00,4d,00,5c,00,77,00,31,00,30,00,5c,00,61,00,6d,00,64,00,36,00,34,00,00,00 30 | "Provider"=hex(1):52,00,65,00,64,00,20,00,48,00,61,00,74,00,2c,00,20,00,49,00,6e,00,63,00,2e,00,00,00 31 | "SignerName"=hex(1):00,00 32 | "SignerScore"=dword:0d000004 33 | "StatusFlags"=dword:00000012 34 | "Version"=hex(3):00,ff,09,00,00,00,00,00,72,e9,36,4d,25,e3,ce,11,bf,c1,08,00,2b,e1,03,18,00,00,8e,c3,86,b8,d6,01,38,4a,68,00,53,00,64,00,00,00,00,00,00,00,00,00 35 | `, 36 | } 37 | -------------------------------------------------------------------------------- /sources/openeuler-http_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetLatestRelease(t *testing.T) { 11 | s := &openEuler{} 12 | s.client = http.DefaultClient 13 | 14 | tests := []struct { 15 | url string 16 | release string 17 | want string 18 | shouldFail bool 19 | }{ 20 | { 21 | "https://repo.openeuler.org/", 22 | "22.03", 23 | "22.03-LTS-SP4", 24 | false, 25 | }, 26 | { 27 | "https://repo.openeuler.org/", 28 | "20.03", 29 | "20.03-LTS-SP4", 30 | false, 31 | }, 32 | { 33 | "https://repo.openeuler.org/", 34 | "20.03-LTS", 35 | "20.03-LTS-SP4", 36 | false, 37 | }, 38 | { 39 | "https://repo.openeuler.org/", 40 | "20.03-LTS-SP1", 41 | "20.03-LTS-SP1", 42 | false, 43 | }, 44 | { 45 | "https://repo.openeuler.org/", 46 | "21.03", 47 | "21.03", 48 | false, 49 | }, 50 | { 51 | "https://repo.openeuler.org/", 52 | "22.00", // non-existed release 53 | "", 54 | true, 55 | }, 56 | { 57 | "https://repo.openeuler.org/", 58 | "BadRelease", // invalid format 59 | "", 60 | true, 61 | }, 62 | { 63 | "https://repo.openeuler.org/", 64 | "", // null string 65 | "", 66 | true, 67 | }, 68 | { 69 | "foobar", // invalid url 70 | "22.03", 71 | "", 72 | true, 73 | }, 74 | } 75 | 76 | for _, test := range tests { 77 | release, err := s.getLatestRelease(test.url, test.release) 78 | if test.shouldFail { 79 | require.NotNil(t, err) 80 | } else { 81 | require.NoError(t, err) 82 | require.NotEmpty(t, release) 83 | require.Equal(t, test.want, release) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sources/testdata/key5.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBE4P06MBEACqn48FZgYkG2QrtUAVDV58H6LpDYEcTcv4CIFSkgs6dJ9TavCW 4 | NyPBZRpM2R+Rg5eVqlborp7TmktBP/sSsxc8eJ+3P2aQWSWc5ol74Y0OznJUCrBr 5 | bIdypJllsD9Fe+h7gLBXTh3vdBEWr2lR+xA+Oou8UlO2gFbVFQqMafUgU1s0vqaE 6 | /hHH0TzwD0/tJ6eqIbHwVR/Bu6kHFK4PwePovhfvyYD9Y+C0vOYd5Ict2vbLHz1f 7 | QBDZObv4M6KN3j7nzme47hKtdMd+LwFqxM5cXfM6b5doDulWPmuGV78VoX6OR7el 8 | x1tlfpuiFeuXYnImm5nTawArcQ1UkXUSYcTUKShJebRDLR3BycxR39Q9jtbOQ29R 9 | FumHginovEhdUcinRr22eRXgcmzpR00zFIWoFCwHh/OCtG14nFhefuZ8Z80qbVhW 10 | 2J9+/O4tksv9HtQBmQNOK5S8C4HNF2M8AfOWNTr8esFSDc0YA5/cxzdfOOtWam/w 11 | lBpNcUUSSgddRsBwijPuWhVA3NmA/uQlJtAo4Ji5vo8cj5MTPG3+U+rfNqRxu1Yc 12 | ioXRo4LzggPscaTZX6V24n0fzw0J2k7TT4sX007k+7YXwEMqmHpcMYbDNzdCzUer 13 | Zilh5hihJwvGfdi234W3GofttoO+jaAZjic7a3p6cO1ICMgfVqrbZCUQVQARAQAB 14 | tEZDZW50T1MtNiBLZXkgKENlbnRPUyA2IE9mZmljaWFsIFNpZ25pbmcgS2V5KSA8 15 | Y2VudG9zLTYta2V5QGNlbnRvcy5vcmc+iQI8BBMBAgAmBQJOD9OjAhsDBQkSzAMA 16 | BgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQCUb8osEFud6ajRAAnb6d+w6Y/v/d 17 | MSy7UEy4rNquArix8xhqBwwjoGXpa37OqTvvcJrftZ1XgtzmTbkqXc+9EFch0C+w 18 | ST10f+H0SPTUGuPwqLkg27snUkDAv1B8laub+l2L9erzCaRriH8MnFyxt5v1rqWA 19 | mVlRymzgXK+EQDr+XOgMm1CvxVY3OwdjdoHNox4TdVQWlZl83xdLXBxkd5IRciNm 20 | sg5fJAzAMeg8YsoDee3m4khg9gEm+/Rj5io8Gfk0nhQpgGGeS1HEXl5jzTb44zQW 21 | qudkfcLEdUMOECbu7IC5Z1wrcj559qcp9C94IwQQO+LxLwg4kHffvZjCaOXDRiya 22 | h8KGsEDuiqwjU9HgGq9fa0Ceo3OyUazUi+WnOxBLVIQ8cUZJJ2Ia5PDnEsz59kCp 23 | JmBZaYPxUEteMtG3yDTa8c8jUnJtMPpkwpSkeMBeNr/rEH4YcBoxuFjppHzQpJ7G 24 | hZRbOfY8w97TgJbfDElwTX0/xX9ypsmBezgGoOvOkzP9iCy9YUBc9q/SNnflRWPO 25 | sMVrjec0vc6ffthu2xBdigBXhL7x2bphWzTXf2T067k+JOdoh5EGney6LhQzcp8m 26 | YCTENStCR+L/5XwrvNgRBnoXe4e0ZHet1CcCuBCBvSmsPHp5ml21ahsephnHx+rl 27 | JNGtzulnNP07RyfzQcpCNFH7W4lXzqM= 28 | =jrWY 29 | -----END PGP PUBLIC KEY BLOCK----- 30 | -------------------------------------------------------------------------------- /managers/manager_test.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | func TestManagePackages(t *testing.T) { 12 | sets := []shared.DefinitionPackagesSet{ 13 | { 14 | Packages: []string{"foo"}, 15 | Action: "install", 16 | }, 17 | { 18 | Packages: []string{"bar"}, 19 | Action: "install", 20 | }, 21 | { 22 | Packages: []string{"baz"}, 23 | Action: "remove", 24 | }, 25 | { 26 | Packages: []string{"lorem"}, 27 | Action: "remove", 28 | }, 29 | { 30 | Packages: []string{"ipsum"}, 31 | Action: "install", 32 | }, 33 | { 34 | Packages: []string{"dolor"}, 35 | Action: "remove", 36 | }, 37 | } 38 | 39 | optimizedSets := optimizePackageSets(sets) 40 | require.Len(t, optimizedSets, 4) 41 | require.Equal(t, optimizedSets[0], shared.DefinitionPackagesSet{Action: "install", Packages: []string{"foo", "bar"}}) 42 | require.Equal(t, optimizedSets[1], shared.DefinitionPackagesSet{Action: "remove", Packages: []string{"baz", "lorem"}}) 43 | require.Equal(t, optimizedSets[2], shared.DefinitionPackagesSet{Action: "install", Packages: []string{"ipsum"}}) 44 | require.Equal(t, optimizedSets[3], shared.DefinitionPackagesSet{Action: "remove", Packages: []string{"dolor"}}) 45 | 46 | sets = []shared.DefinitionPackagesSet{ 47 | { 48 | Packages: []string{"foo"}, 49 | Action: "install", 50 | }, 51 | } 52 | 53 | optimizedSets = optimizePackageSets(sets) 54 | require.Len(t, optimizedSets, 1) 55 | require.Equal(t, optimizedSets[0], shared.DefinitionPackagesSet{Action: "install", Packages: []string{"foo"}}) 56 | 57 | sets = []shared.DefinitionPackagesSet{} 58 | optimizedSets = optimizePackageSets(sets) 59 | require.Len(t, optimizedSets, 0) 60 | } 61 | -------------------------------------------------------------------------------- /generators/generators.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/lxc/distrobuilder/image" 9 | "github.com/lxc/distrobuilder/shared" 10 | ) 11 | 12 | // ErrNotSupported returns a "Not supported" error. 13 | var ErrNotSupported = errors.New("Not supported") 14 | 15 | // ErrUnknownGenerator represents the unknown generator error. 16 | var ErrUnknownGenerator = errors.New("Unknown generator") 17 | 18 | type generator interface { 19 | init(logger *logrus.Logger, cacheDir string, sourceDir string, defFile shared.DefinitionFile, def shared.Definition) 20 | 21 | Generator 22 | } 23 | 24 | // Generator interface. 25 | type Generator interface { 26 | RunLXC(*image.LXCImage, shared.DefinitionTargetLXC) error 27 | RunIncus(*image.IncusImage, shared.DefinitionTargetIncus) error 28 | Run() error 29 | } 30 | 31 | var generators = map[string]func() generator{ 32 | "cloud-init": func() generator { return &cloudInit{} }, 33 | "copy": func() generator { return ©{} }, 34 | "dump": func() generator { return &dump{} }, 35 | "fstab": func() generator { return &fstab{} }, 36 | "hostname": func() generator { return &hostname{} }, 37 | "hosts": func() generator { return &hosts{} }, 38 | "incus-agent": func() generator { return &incusAgent{} }, 39 | "remove": func() generator { return &remove{} }, 40 | "template": func() generator { return &template{} }, 41 | } 42 | 43 | // Load loads and initializes a generator. 44 | func Load(generatorName string, logger *logrus.Logger, cacheDir string, sourceDir string, defFile shared.DefinitionFile, def shared.Definition) (Generator, error) { 45 | df, ok := generators[generatorName] 46 | if !ok { 47 | return nil, ErrUnknownGenerator 48 | } 49 | 50 | d := df() 51 | 52 | d.init(logger, cacheDir, sourceDir, defFile, def) 53 | 54 | return d, nil 55 | } 56 | -------------------------------------------------------------------------------- /windows/testdata/2k12r2_install_wim_info.txt: -------------------------------------------------------------------------------- 1 | WIM Information: 2 | ---------------- 3 | Path: mnt/sources/install.wim 4 | GUID: 0x93729e9c55a84446aa29e70356e722ef 5 | Version: 68864 6 | Image Count: 1 7 | Compression: LZX 8 | Chunk Size: 32768 bytes 9 | Part Number: 1/1 10 | Boot Index: 0 11 | Size: 3468252747 bytes 12 | Attributes: Integrity info, Relative path junction 13 | 14 | Available Images: 15 | ----------------- 16 | Index: 1 17 | Name: Windows Server 2012 R2 SERVERSOLUTION 18 | Description: Windows Server 2012 R2 SERVERSOLUTION 19 | Display Name: Windows Server 2012 R2 Essentials 20 | Display Description: This option is useful when a GUI is required—for example, to provide backward compatibility for an application that cannot be run on a Server Core installation. All server roles and features are supported. You can switch to a different installation option later. See "Windows Server Installation Options." 21 | Directory Count: 18861 22 | File Count: 80441 23 | Total Bytes: 12568990555 24 | Hard Link Bytes: 5015987674 25 | Creation Time: Thu Aug 22 16:18:40 2013 UTC 26 | Last Modification Time: Thu Aug 22 16:19:13 2013 UTC 27 | Architecture: x86_64 28 | Product Name: Microsoft® Windows® Operating System 29 | Edition ID: ServerSolution 30 | Installation Type: Server 31 | HAL: acpiapic 32 | Product Type: ServerNT 33 | Product Suite: Terminal Server 34 | Languages: en-US 35 | Default Language: en-US 36 | System Root: WINDOWS 37 | Major Version: 6 38 | Minor Version: 3 39 | Build: 9600 40 | Service Pack Build: 16384 41 | Service Pack Level: 0 42 | Flags: ServerSolution 43 | WIMBoot compatible: no 44 | -------------------------------------------------------------------------------- /managers/equo.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lxc/distrobuilder/shared" 7 | ) 8 | 9 | type equo struct { 10 | common 11 | } 12 | 13 | func (m *equo) load() error { 14 | m.commands = managerCommands{ 15 | clean: "equo", 16 | install: "equo", 17 | refresh: "equo", 18 | remove: "equo", 19 | update: "equo", 20 | } 21 | 22 | m.flags = managerFlags{ 23 | global: []string{}, 24 | clean: []string{ 25 | "cleanup", 26 | }, 27 | install: []string{ 28 | "install", 29 | }, 30 | remove: []string{ 31 | "remove", 32 | }, 33 | refresh: []string{ 34 | "update", 35 | }, 36 | update: []string{ 37 | "upgrade", 38 | }, 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (m *equo) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 45 | switch repoAction.Type { 46 | case "", "equo": 47 | return m.equoRepoCaller(repoAction) 48 | case "enman": 49 | return m.enmanRepoCaller(repoAction) 50 | } 51 | 52 | return errors.New("Invalid repository Type") 53 | } 54 | 55 | func (m *equo) enmanRepoCaller(repo shared.DefinitionPackagesRepository) error { 56 | args := []string{ 57 | "add", 58 | } 59 | 60 | if repo.Name == "" && repo.URL == "" { 61 | return errors.New("Missing both repository url and repository name") 62 | } 63 | 64 | if repo.URL != "" { 65 | args = append(args, repo.URL) 66 | } else { 67 | args = append(args, repo.Name) 68 | } 69 | 70 | return shared.RunCommand(m.ctx, nil, nil, "enman", args...) 71 | } 72 | 73 | func (m *equo) equoRepoCaller(repo shared.DefinitionPackagesRepository) error { 74 | if repo.Name == "" { 75 | return errors.New("Invalid repository name") 76 | } 77 | 78 | if repo.URL == "" { 79 | return errors.New("Invalid repository url") 80 | } 81 | 82 | return shared.RunCommand(m.ctx, nil, nil, "equo", "repo", "add", "--repo", repo.URL, "--pkg", repo.URL, 83 | repo.Name) 84 | } 85 | -------------------------------------------------------------------------------- /generators/dump.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/lxc/distrobuilder/image" 10 | "github.com/lxc/distrobuilder/shared" 11 | ) 12 | 13 | type dump struct { 14 | common 15 | } 16 | 17 | // RunLXC dumps content to a file. 18 | func (g *dump) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error { 19 | content := g.defFile.Content 20 | 21 | err := g.run(content) 22 | if err != nil { 23 | return fmt.Errorf("Failed to dump content: %w", err) 24 | } 25 | 26 | if g.defFile.Templated { 27 | err = img.AddTemplate(g.defFile.Path) 28 | if err != nil { 29 | return fmt.Errorf("Failed to add template: %w", err) 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // RunIncus dumps content to a file. 37 | func (g *dump) RunIncus(img *image.IncusImage, target shared.DefinitionTargetIncus) error { 38 | content := g.defFile.Content 39 | 40 | return g.run(content) 41 | } 42 | 43 | // Run dumps content to a file. 44 | func (g *dump) Run() error { 45 | return g.run(g.defFile.Content) 46 | } 47 | 48 | func (g *dump) run(content string) error { 49 | path := filepath.Join(g.sourceDir, g.defFile.Path) 50 | 51 | // Create any missing directory 52 | err := os.MkdirAll(filepath.Dir(path), 0o755) 53 | if err != nil { 54 | return fmt.Errorf("Failed to create directory %q: %w", filepath.Dir(path), err) 55 | } 56 | 57 | // Open the target file (create if needed) 58 | file, err := os.Create(path) 59 | if err != nil { 60 | return fmt.Errorf("Failed to create file %q: %w", path, err) 61 | } 62 | 63 | defer file.Close() 64 | 65 | // Append final new line if missing 66 | if !strings.HasSuffix(content, "\n") { 67 | content += "\n" 68 | } 69 | 70 | // Write the content 71 | _, err = file.WriteString(content) 72 | if err != nil { 73 | return fmt.Errorf("Failed to write string to file %q: %w", path, err) 74 | } 75 | 76 | err = updateFileAccess(file, g.defFile) 77 | if err != nil { 78 | return fmt.Errorf("Failed to update file access of %q: %w", path, err) 79 | } 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /sources/alpaquita-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "crypto/sha512" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | type alpaquita struct { 12 | common 13 | } 14 | 15 | func (s *alpaquita) Run() error { 16 | baseURL, fname, err := s.getMiniroot(s.definition) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | var fpath string 22 | tarballURL := baseURL + fname 23 | if s.definition.Source.SkipVerification { 24 | fpath, err = s.DownloadHash(s.definition.Image, 25 | tarballURL, "", nil) 26 | if err != nil { 27 | return err 28 | } 29 | } else { 30 | fpath, err = s.DownloadHash(s.definition.Image, 31 | tarballURL, tarballURL+".sha512", sha512.New()) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | tarballLocal := filepath.Join(fpath, fname) 38 | s.logger.WithField("file", tarballLocal).Info("Unpacking image") 39 | 40 | err = shared.Unpack(tarballLocal, s.rootfsDir) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // Sample URLs (or with "latest" instead of date): 49 | // 50 | // https://packages.bell-sw.com/alpaquita/musl/stream/releases/x86_64/alpaquita-minirootfs-stream-241231-musl-x86_64.tar.gz 51 | // https://packages.bell-sw.com/alpaquita/glibc/23/releases/aarch64/alpaquita-minirootfs-23-241231-glibc-aarch64.tar.gz 52 | func (s *alpaquita) getMiniroot(definition shared.Definition) (string, string, error) { 53 | // default server 54 | if s.definition.Source.URL == "" { 55 | s.definition.Source.URL = "https://packages.bell-sw.com" 56 | } 57 | 58 | // require explicit source variant (libc) 59 | if s.definition.Source.Variant == "" { 60 | return "", "", fmt.Errorf("Alpaquita requires explicitly specified source variant") 61 | } 62 | 63 | base := fmt.Sprintf("%s/alpaquita/%s/%s/releases/%s/", 64 | s.definition.Source.URL, 65 | s.definition.Source.Variant, 66 | s.definition.Image.Release, 67 | s.definition.Image.ArchitectureMapped) 68 | 69 | fname := fmt.Sprintf("alpaquita-minirootfs-%s-latest-%s-%s.tar.gz", 70 | s.definition.Image.Release, 71 | s.definition.Source.Variant, 72 | s.definition.Image.ArchitectureMapped) 73 | 74 | return base, fname, nil 75 | } 76 | -------------------------------------------------------------------------------- /distrobuilder/vm_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | func lsblkOutputHelper(t *testing.T, v *vm, args [][]string) func() { 12 | t.Helper() 13 | // Prepare image file 14 | f, err := os.CreateTemp("", "lsblkOutput*.raw") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | err = f.Close() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | v.imageFile = f.Name() 25 | v.size = 2e8 26 | defer func() { 27 | if err != nil { 28 | os.RemoveAll(v.imageFile) 29 | } 30 | }() 31 | 32 | err = v.createEmptyDiskImage() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | v.ctx = context.Background() 38 | // Format disk 39 | if args == nil { 40 | err = v.createPartitions() 41 | } else { 42 | err = v.createPartitions(args...) 43 | } 44 | 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | // losetup 50 | err = v.losetup() 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | return func() { 56 | err := shared.RunCommand(v.ctx, nil, nil, "losetup", "-d", v.loopDevice) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | err = os.RemoveAll(v.imageFile) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | } 66 | } 67 | 68 | func TestLsblkOutput(t *testing.T) { 69 | if os.Getuid() != 0 { 70 | t.Skip() 71 | } 72 | 73 | tcs := []struct { 74 | name string 75 | args [][]string 76 | want int 77 | }{ 78 | {"DiskEmpty", [][]string{{"--zap-all"}}, 1}, 79 | {"UEFI", nil, 3}, 80 | {"MBR", [][]string{{"--new=1::"}, {"--gpttombr"}}, 2}, 81 | } 82 | 83 | for _, tc := range tcs { 84 | t.Run(tc.name, func(t *testing.T) { 85 | v := &vm{} 86 | rb := lsblkOutputHelper(t, v, tc.args) 87 | defer rb() 88 | parse, num, err := v.lsblkLoopDevice() 89 | if err != nil || num != tc.want { 90 | t.Fatal(err, num, tc.want) 91 | } 92 | 93 | for i := 0; i < num; i++ { 94 | major, minor, err := parse(i) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | if major == 0 && minor == 0 { 100 | t.Fatal(major, minor) 101 | } 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /windows/testdata/w10_boot_wim_info.txt: -------------------------------------------------------------------------------- 1 | WIM Information: 2 | ---------------- 3 | Path: sources/boot.wim 4 | GUID: 0x4f947169f6bbed469d0e2b31835e905a 5 | Version: 68864 6 | Image Count: 2 7 | Compression: LZX 8 | Chunk Size: 32768 bytes 9 | Part Number: 1/1 10 | Boot Index: 2 11 | Size: 554679257 bytes 12 | Attributes: Relative path junction 13 | 14 | Available Images: 15 | ----------------- 16 | Index: 1 17 | Name: Microsoft Windows PE (x64) 18 | Description: Microsoft Windows PE (x64) 19 | Directory Count: 3906 20 | File Count: 17894 21 | Total Bytes: 1863578357 22 | Hard Link Bytes: 879328479 23 | Creation Time: Sat Sep 15 13:29:29 2018 UTC 24 | Last Modification Time: Wed Mar 06 01:31:43 2019 UTC 25 | Architecture: x86_64 26 | Product Name: Microsoft® Windows® Operating System 27 | Edition ID: WindowsPE 28 | Installation Type: WindowsPE 29 | Product Type: WinNT 30 | Languages: zh-CN 31 | Default Language: zh-CN 32 | System Root: WINDOWS 33 | Major Version: 10 34 | Minor Version: 0 35 | Build: 17763 36 | Service Pack Build: 316 37 | Service Pack Level: 0 38 | Flags: 9 39 | WIMBoot compatible: no 40 | 41 | Index: 2 42 | Name: Microsoft Windows Setup (x64) 43 | Description: Microsoft Windows Setup (x64) 44 | Directory Count: 3942 45 | File Count: 18369 46 | Total Bytes: 2031424470 47 | Hard Link Bytes: 960282446 48 | Creation Time: Sat Sep 15 13:29:51 2018 UTC 49 | Last Modification Time: Wed Mar 06 01:34:13 2019 UTC 50 | Architecture: x86_64 51 | Product Name: Microsoft® Windows® Operating System 52 | Edition ID: WindowsPE 53 | Installation Type: WindowsPE 54 | Product Type: WinNT 55 | Languages: zh-CN 56 | Default Language: zh-CN 57 | System Root: WINDOWS 58 | Major Version: 10 59 | Minor Version: 0 60 | Build: 17763 61 | Service Pack Build: 316 62 | Service Pack Level: 0 63 | Flags: 2 64 | WIMBoot compatible: no 65 | -------------------------------------------------------------------------------- /managers/anise.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | incus "github.com/lxc/incus/v6/shared/util" 11 | 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | type anise struct { 16 | common 17 | } 18 | 19 | func (m *anise) load() error { 20 | m.commands = managerCommands{ 21 | clean: "anise", 22 | install: "anise", 23 | refresh: "anise", 24 | remove: "anise", 25 | update: "anise", 26 | } 27 | 28 | m.flags = managerFlags{ 29 | global: []string{}, 30 | clean: []string{ 31 | "cleanup", "--purge-repos", 32 | }, 33 | install: []string{ 34 | // Forcing always override of the protected 35 | // files. Not needed on image creation. 36 | "install", "--skip-config-protect", 37 | }, 38 | refresh: []string{ 39 | "repo", "update", "--force", 40 | }, 41 | remove: []string{ 42 | "uninstall", "--skip-config-protect", 43 | }, 44 | update: []string{ 45 | "upgrade", "--sync-repos", "--skip-config-protect", 46 | }, 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (m *anise) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 53 | var targetFile string 54 | 55 | if repoAction.Name == "" { 56 | return errors.New("Invalid repository name") 57 | } 58 | 59 | if repoAction.URL == "" { 60 | return errors.New("Invalid repository url") 61 | } 62 | 63 | if strings.HasSuffix(repoAction.Name, ".yml") { 64 | targetFile = filepath.Join("/etc/anise/repos.conf.d", repoAction.Name) 65 | } else { 66 | targetFile = filepath.Join("/etc/anise/repos.conf.d", repoAction.Name+".yml") 67 | } 68 | 69 | if !incus.PathExists(filepath.Dir(targetFile)) { 70 | err := os.MkdirAll(filepath.Dir(targetFile), 0o755) 71 | if err != nil { 72 | return fmt.Errorf("Failed to create directory %q: %w", filepath.Dir(targetFile), err) 73 | } 74 | } 75 | 76 | f, err := os.OpenFile(targetFile, os.O_CREATE|os.O_WRONLY, 0o644) 77 | if err != nil { 78 | return fmt.Errorf("Failed to open file %q: %w", targetFile, err) 79 | } 80 | 81 | defer f.Close() 82 | 83 | // NOTE: repo.URL is not an URL but the content of the file. 84 | _, err = f.WriteString(repoAction.URL) 85 | if err != nil { 86 | return fmt.Errorf("Failed to write string to %q: %w", targetFile, err) 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /sources/alt-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "crypto/sha256" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/lxc/distrobuilder/shared" 12 | ) 13 | 14 | type altLinux struct { 15 | common 16 | } 17 | 18 | func (s *altLinux) Run() error { 19 | arch := s.definition.Image.ArchitectureMapped 20 | 21 | if arch == "armhf" { 22 | arch = "armh" 23 | } 24 | 25 | baseURL := fmt.Sprintf( 26 | "%s/%s/cloud/%s/", 27 | s.definition.Source.URL, 28 | s.definition.Image.Release, 29 | arch, 30 | ) 31 | fname := fmt.Sprintf("alt-%s-rootfs-systemd-%s.tar.xz", strings.ToLower(s.definition.Image.Release), arch) 32 | 33 | url, err := url.Parse(baseURL) 34 | if err != nil { 35 | return fmt.Errorf("Failed to parse URL %q: %w", baseURL, err) 36 | } 37 | 38 | checksumFile := "" 39 | 40 | if !s.definition.Source.SkipVerification { 41 | if len(s.definition.Source.Keys) != 0 { 42 | checksumFile = baseURL + "SHA256SUMS" 43 | 44 | fpath, err := s.DownloadHash(s.definition.Image, checksumFile+".gpg", "", nil) 45 | if err != nil { 46 | return fmt.Errorf("Failed to download %q: %w", checksumFile+".gpg", err) 47 | } 48 | 49 | _, err = s.DownloadHash(s.definition.Image, checksumFile, "", nil) 50 | if err != nil { 51 | return fmt.Errorf("Failed to download %q: %w", checksumFile, err) 52 | } 53 | 54 | valid, err := s.VerifyFile( 55 | filepath.Join(fpath, "SHA256SUMS"), 56 | filepath.Join(fpath, "SHA256SUMS.gpg")) 57 | if err != nil { 58 | return fmt.Errorf("Failed to verify file: %w", err) 59 | } 60 | 61 | if !valid { 62 | return fmt.Errorf("Invalid signature for %q", "SHA256SUMS") 63 | } 64 | } else { 65 | // Force gpg checks when using http 66 | if url.Scheme != "https" { 67 | return errors.New("GPG keys are required if downloading from HTTP") 68 | } 69 | } 70 | } 71 | 72 | fpath, err := s.DownloadHash(s.definition.Image, baseURL+fname, checksumFile, sha256.New()) 73 | if err != nil { 74 | return fmt.Errorf("Failed to download %q: %w", baseURL+fname, err) 75 | } 76 | 77 | s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image") 78 | 79 | // Unpack 80 | err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir) 81 | if err != nil { 82 | return fmt.Errorf("Failed to unpack %q: %w", fname, err) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /generators/template.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/flosch/pongo2/v4" 10 | "github.com/lxc/incus/v6/shared/api" 11 | 12 | "github.com/lxc/distrobuilder/image" 13 | "github.com/lxc/distrobuilder/shared" 14 | ) 15 | 16 | type template struct { 17 | common 18 | } 19 | 20 | // RunLXC dumps content to a file. 21 | func (g *template) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error { 22 | // no template support for LXC, ignoring generator 23 | return nil 24 | } 25 | 26 | // RunIncus dumps content to a file. 27 | func (g *template) RunIncus(img *image.IncusImage, target shared.DefinitionTargetIncus) error { 28 | templateDir := filepath.Join(g.cacheDir, "templates") 29 | 30 | err := os.MkdirAll(templateDir, 0o755) 31 | if err != nil { 32 | return fmt.Errorf("Failed to create directory %q: %w", templateDir, err) 33 | } 34 | 35 | template := fmt.Sprintf("%s.tpl", g.defFile.Name) 36 | 37 | file, err := os.Create(filepath.Join(templateDir, template)) 38 | if err != nil { 39 | return fmt.Errorf("Failed to create file %q: %w", filepath.Join(templateDir, template), err) 40 | } 41 | 42 | defer file.Close() 43 | 44 | content := g.defFile.Content 45 | 46 | // Append final new line if missing 47 | if !strings.HasSuffix(content, "\n") { 48 | content += "\n" 49 | } 50 | 51 | if g.defFile.Pongo { 52 | tpl, err := pongo2.FromString(content) 53 | if err != nil { 54 | return fmt.Errorf("Failed to parse template: %w", err) 55 | } 56 | 57 | content, err = tpl.Execute(pongo2.Context{"incus": target}) 58 | if err != nil { 59 | return fmt.Errorf("Failed to execute template: %w", err) 60 | } 61 | } 62 | 63 | _, err = file.WriteString(content) 64 | if err != nil { 65 | return fmt.Errorf("Failed to write to content to %s template: %w", g.defFile.Name, err) 66 | } 67 | 68 | // Add to Incus templates 69 | img.Metadata.Templates[g.defFile.Path] = &api.ImageMetadataTemplate{ 70 | Template: template, 71 | Properties: g.defFile.Template.Properties, 72 | When: g.defFile.Template.When, 73 | } 74 | 75 | if len(g.defFile.Template.When) == 0 { 76 | img.Metadata.Templates[g.defFile.Path].When = []string{ 77 | "create", 78 | "copy", 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Run does nothing. 86 | func (g *template) Run() error { 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /managers/apk.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | type apk struct { 12 | common 13 | } 14 | 15 | func (m *apk) load() error { 16 | m.commands = managerCommands{ 17 | clean: "apk", 18 | install: "apk", 19 | refresh: "apk", 20 | remove: "apk", 21 | update: "apk", 22 | } 23 | 24 | m.flags = managerFlags{ 25 | global: []string{ 26 | "--no-cache", 27 | }, 28 | install: []string{ 29 | "add", 30 | }, 31 | remove: []string{ 32 | "del", "--rdepends", 33 | }, 34 | refresh: []string{ 35 | "update", 36 | }, 37 | update: []string{ 38 | "upgrade", 39 | }, 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (m *apk) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 46 | err := m.appendRepositoryURL(repoAction) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | err = m.writeKeyFile(repoAction) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (m *apk) appendRepositoryURL(repoAction shared.DefinitionPackagesRepository) error { 60 | if repoAction.URL == "" { 61 | return nil 62 | } 63 | 64 | repoFile := "/etc/apk/repositories" 65 | 66 | f, err := os.OpenFile(repoFile, os.O_WRONLY|os.O_APPEND, 0o644) 67 | if err != nil { 68 | return fmt.Errorf("Failed to open %q: %w", repoFile, err) 69 | } 70 | 71 | defer f.Close() 72 | 73 | _, err = f.WriteString(repoAction.URL + "\n") 74 | if err != nil { 75 | return fmt.Errorf("Failed to write string to file: %w", err) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (m *apk) writeKeyFile(repoAction shared.DefinitionPackagesRepository) error { 82 | if repoAction.Key == "" || repoAction.Name == "" { 83 | return nil 84 | } 85 | 86 | if strings.Contains(repoAction.Name, "/") { 87 | return fmt.Errorf("Invalid key file name: %q", repoAction.Name) 88 | } 89 | 90 | keyFile := "/etc/apk/keys/" + repoAction.Name 91 | f, err := os.OpenFile(keyFile, os.O_CREATE|os.O_WRONLY, 0o644) 92 | if err != nil { 93 | return fmt.Errorf("Failed to open %q: %w", keyFile, err) 94 | } 95 | 96 | defer f.Close() 97 | 98 | _, err = f.WriteString(repoAction.Key + "\n") 99 | if err != nil { 100 | return fmt.Errorf("Failed to write %q: %w", keyFile, err) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /windows/driver_viogpudo.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | var driverVioGPUDo = DriverInfo{ 4 | PackageName: "viogpudo.inf_amd64_8224060246e67964", 5 | DriversRegistry: `[\DriverDatabase\DeviceIds\{{ classGuid|lower }}] 6 | "{{ infFile }}"=hex(0): 7 | 8 | [\DriverDatabase\DeviceIds\pci\VEN_1AF4&DEV_1050&SUBSYS_11001AF4&REV_01] 9 | "{{ infFile }}"=hex(3):01,f9,00,00 10 | 11 | [\DriverDatabase\DriverInfFiles\{{ infFile }}] 12 | @=hex(7):{{ packageName|toHex }},00,00,00,00 13 | "Active"=hex(1):{{ packageName|toHex }},00,00 14 | 15 | [\DriverDatabase\DriverPackages\{{ packageName }}] 16 | @=hex(1):6f,00,65,00,6d,00,30,00,2e,00,69,00,6e,00,66,00,00,00 17 | "Catalog"=hex(1):76,00,69,00,6f,00,67,00,70,00,75,00,64,00,6f,00,2e,00,63,00,61,00,74,00,00,00 18 | "ImportDate"=hex(3):80,13,e8,66,f6,9d,d7,01 19 | "InfName"=hex(1):76,00,69,00,6f,00,67,00,70,00,75,00,64,00,6f,00,2e,00,69,00,6e,00,66,00,00,00 20 | "OemPath"=hex(1):5c,00,5c,00,31,00,39,00,32,00,2e,00,31,00,36,00,38,00,2e,00,31,00,37,00,38,00,2e,00,37,00,30,00,5c,00,73,00,68,00,61,00,72,00,65,00,64,00,5c,00,76,00,69,00,72,00,74,00,69,00,6f,00,5c,00,76,00,69,00,6f,00,67,00,70,00,75,00,64,00,6f,00,5c,00,77,00,31,00,30,00,5c,00,61,00,6d,00,64,00,36,00,34,00,00,00 21 | "Provider"=hex(1):52,00,65,00,64,00,20,00,48,00,61,00,74,00,2c,00,20,00,49,00,6e,00,63,00,2e,00,00,00 22 | "SignerName"=hex(1):4d,00,69,00,63,00,72,00,6f,00,73,00,6f,00,66,00,74,00,20,00,57,00,69,00,6e,00,64,00,6f,00,77,00,73,00,20,00,48,00,61,00,72,00,64,00,77,00,61,00,72,00,65,00,20,00,43,00,6f,00,6d,00,70,00,61,00,74,00,69,00,62,00,69,00,6c,00,69,00,74,00,79,00,20,00,50,00,75,00,62,00,6c,00,69,00,73,00,68,00,65,00,72,00,00,00 23 | "SignerScore"=dword:0d000005 24 | "StatusFlags"=dword:00000512 25 | "Version"=hex(3):00,ff,09,00,00,00,00,00,68,e9,36,4d,25,e3,ce,11,bf,c1,08,00,2b,e1,03,18,00,40,ef,05,7a,77,d7,01,b0,4f,68,00,55,00,64,00,00,00,00,00,00,00,00,00 26 | 27 | [\DriverDatabase\DriverPackages\{{ packageName }}\Properties] 28 | 29 | [\DriverDatabase\DriverPackages\{{ packageName }}\Properties\{4da162c1-5eb1-4140-a444-5064c9814e76}] 30 | 31 | [\DriverDatabase\DriverPackages\{{ packageName }}\Properties\{4da162c1-5eb1-4140-a444-5064c9814e76}\0009] 32 | @=hex(ffff0012):33,00,30,00,30,00,39,00,37,00,37,00,37,00,30,00,5f,00,31,00,34,00,31,00,35,00,35,00,36,00,33,00,31,00,34,00,35,00,36,00,37,00,30,00,36,00,35,00,39,00,32,00,5f,00,31,00,31,00,35,00,32,00,39,00,32,00,31,00,35,00,30,00,35,00,36,00,39,00,33,00,36,00,38,00,33,00,38,00,34,00,38,00,00,00 33 | `, 34 | } 35 | -------------------------------------------------------------------------------- /distrobuilder/chroot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/sirupsen/logrus" 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func getOverlay(logger *logrus.Logger, cacheDir, sourceDir string) (func(), string, error) { 14 | var stat unix.Statfs_t 15 | 16 | // Skip overlay on xfs and zfs 17 | for _, dir := range []string{cacheDir, sourceDir} { 18 | err := unix.Statfs(dir, &stat) 19 | if err != nil { 20 | return nil, "", err 21 | } 22 | 23 | switch stat.Type { 24 | case unix.XFS_SUPER_MAGIC: 25 | return nil, "", errors.New("overlay not supported on xfs") 26 | case 0x2fc12fc1: 27 | return nil, "", errors.New("overlay not supported on zfs") 28 | } 29 | } 30 | 31 | upperDir := filepath.Join(cacheDir, "upper") 32 | overlayDir := filepath.Join(cacheDir, "overlay") 33 | workDir := filepath.Join(cacheDir, "work") 34 | 35 | err := os.Mkdir(upperDir, 0o755) 36 | if err != nil { 37 | return nil, "", fmt.Errorf("Failed to create directory %q: %w", upperDir, err) 38 | } 39 | 40 | err = os.Mkdir(overlayDir, 0o755) 41 | if err != nil { 42 | return nil, "", fmt.Errorf("Failed to create directory %q: %w", overlayDir, err) 43 | } 44 | 45 | err = os.Mkdir(workDir, 0o755) 46 | if err != nil { 47 | return nil, "", fmt.Errorf("Failed to create directory %q: %w", workDir, err) 48 | } 49 | 50 | opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", sourceDir, upperDir, workDir) 51 | 52 | err = unix.Mount("overlay", overlayDir, "overlay", 0, opts) 53 | if err != nil { 54 | return nil, "", fmt.Errorf("Failed to mount overlay: %w", err) 55 | } 56 | 57 | cleanup := func() { 58 | unix.Sync() 59 | 60 | err := unix.Unmount(overlayDir, 0) 61 | if err != nil { 62 | logger.WithFields(logrus.Fields{"err": err, "dir": overlayDir}).Warn("Failed to unmount overlay directory") 63 | } 64 | 65 | err = os.RemoveAll(upperDir) 66 | if err != nil { 67 | logger.WithFields(logrus.Fields{"err": err, "dir": upperDir}).Warn("Failed to remove upper directory") 68 | } 69 | 70 | err = os.RemoveAll(workDir) 71 | if err != nil { 72 | logger.WithFields(logrus.Fields{"err": err, "dir": workDir}).Warn("Failed to remove work directory") 73 | } 74 | 75 | err = os.Remove(overlayDir) 76 | if err != nil { 77 | logger.WithFields(logrus.Fields{"err": err, "dir": overlayDir}).Warn("Failed to remove overlay directory") 78 | } 79 | } 80 | 81 | return cleanup, overlayDir, nil 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | - push 4 | - pull_request 5 | 6 | permissions: 7 | contents: read 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | code-tests: 15 | name: Code tests 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | go: 20 | - oldstable 21 | - stable 22 | os: 23 | - ubuntu-22.04 24 | - ubuntu-24.04 25 | runs-on: ${{ matrix.os }} 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Dependency Review 32 | uses: actions/dependency-review-action@v4 33 | if: github.event_name == 'pull_request' 34 | with: 35 | allow-ghsas: GHSA-56mx-8g9f-5crf 36 | 37 | - name: Install Go 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version: ${{ matrix.go }} 41 | 42 | - name: Install dependencies 43 | run: | 44 | sudo apt-get -qq update 45 | sudo apt-get install -y \ 46 | pipx \ 47 | squashfs-tools 48 | 49 | # With pipx >= 1.5.0, we could use pipx --global instead. 50 | PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin \ 51 | pipx install codespell 52 | 53 | - name: Run static analysis 54 | run: make static-analysis 55 | 56 | - name: Unit tests (all) 57 | run: make check 58 | 59 | documentation: 60 | name: Documentation tests 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Install dependencies 67 | run: | 68 | sudo apt-get install aspell aspell-en 69 | sudo snap install mdl 70 | 71 | - name: Run markdown linter 72 | run: | 73 | make doc-lint 74 | 75 | - name: Run spell checker 76 | run: | 77 | make doc-spellcheck 78 | 79 | - name: Run inclusive naming checker 80 | uses: get-woke/woke-action@v0 81 | with: 82 | fail-on-error: true 83 | woke-args: "*.md **/*.md -c https://github.com/canonical-web-and-design/Inclusive-naming/raw/main/config.yml" 84 | 85 | - name: Run link checker 86 | # This can fail intermittently due to external resources being unavailable. 87 | continue-on-error: true 88 | run: | 89 | make doc-linkcheck 90 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # `distrobuilder` 2 | 3 | `distrobuilder` is an image building tool for LXC and Incus. 4 | 5 | Its modern design uses pre-built official images whenever available and supports a variety of modifications on the base image. 6 | `distrobuilder` creates LXC or Incus images, or just a plain root file system, from a declarative image definition (in YAML format) that defines the source of the image, its package manager, what packages to install or remove for specific image variants, OS releases and architectures, as well as additional files to generate and arbitrary actions to execute as part of the image build process. 7 | 8 | `distrobuilder` can be used to create custom images that can be used as the base for LXC containers or Incus instances. 9 | 10 | `distrobuilder` is used to build the images on the [Linux containers image server](https://images.linuxcontainers.org/). 11 | You can also use it to build images from ISO files that require licenses and therefore cannot be distributed. 12 | 13 | --- 14 | 15 | ## In this documentation 16 | 17 | ````{grid} 1 1 2 2 18 | ```{grid-item} [](tutorials/index) 19 | 20 | **Start here**: a hands-on introduction to `distrobuilder` for new users 21 | ``` 22 | ```{grid-item} [](howto/index) 23 | 24 | **Step-by-step guides** covering key operations and common tasks 25 | ``` 26 | ```` 27 | 28 | ````{grid} 1 1 2 2 29 | :reverse: 30 | 31 | ```{grid-item} [](reference/index) 32 | 33 | **Technical information** - specifications, APIs, architecture 34 | ``` 35 | 36 | ```{grid-item} Explanation (coming) 37 | 38 | **Discussion and clarification** of key topics 39 | ``` 40 | ```` 41 | 42 | --- 43 | 44 | ## Project and community 45 | 46 | `distrobuilder` is free software and developed under the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0). 47 | It's an open source project that warmly welcomes community projects, contributions, suggestions, fixes and constructive feedback. 48 | 49 | - [Contribute to the project](https://github.com/lxc/distrobuilder/blob/master/CONTRIBUTING.md) 50 | - [Discuss on IRC](https://web.libera.chat/#lxc) (see [Getting started with IRC](https://discuss.linuxcontainers.org/t/getting-started-with-irc/11920) if needed) 51 | - [Ask and answer questions on the forum](https://discuss.linuxcontainers.org) 52 | - [Join the mailing lists](https://lists.linuxcontainers.org) 53 | 54 | ```{toctree} 55 | :hidden: 56 | :titlesonly: 57 | 58 | self 59 | tutorials/index 60 | howto/index 61 | reference/index 62 | ``` 63 | -------------------------------------------------------------------------------- /generators/hostname_test.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/lxc/distrobuilder/image" 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | func TestHostnameGeneratorRunLXC(t *testing.T) { 16 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 17 | require.NoError(t, err) 18 | 19 | rootfsDir := filepath.Join(cacheDir, "rootfs") 20 | 21 | setup(t, cacheDir) 22 | defer teardown(cacheDir) 23 | 24 | generator, err := Load("hostname", nil, cacheDir, rootfsDir, shared.DefinitionFile{Path: "/etc/hostname"}, shared.Definition{}) 25 | require.IsType(t, &hostname{}, generator) 26 | require.NoError(t, err) 27 | 28 | definition := shared.Definition{ 29 | Image: shared.DefinitionImage{ 30 | Distribution: "ubuntu", 31 | Release: "artful", 32 | }, 33 | } 34 | 35 | image := image.NewLXCImage(context.TODO(), cacheDir, "", cacheDir, definition) 36 | 37 | err = os.MkdirAll(filepath.Join(cacheDir, "rootfs", "etc"), 0o755) 38 | require.NoError(t, err) 39 | 40 | createTestFile(t, filepath.Join(cacheDir, "rootfs", "etc", "hostname"), "hostname") 41 | 42 | err = generator.RunLXC(image, shared.DefinitionTargetLXC{}) 43 | require.NoError(t, err) 44 | 45 | validateTestFile(t, filepath.Join(cacheDir, "rootfs", "etc", "hostname"), "LXC_NAME\n") 46 | } 47 | 48 | func TestHostnameGeneratorRunIncus(t *testing.T) { 49 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 50 | require.NoError(t, err) 51 | 52 | rootfsDir := filepath.Join(cacheDir, "rootfs") 53 | 54 | setup(t, cacheDir) 55 | defer teardown(cacheDir) 56 | 57 | generator, err := Load("hostname", nil, cacheDir, rootfsDir, shared.DefinitionFile{Path: "/etc/hostname"}, shared.Definition{}) 58 | require.IsType(t, &hostname{}, generator) 59 | require.NoError(t, err) 60 | 61 | definition := shared.Definition{ 62 | Image: shared.DefinitionImage{ 63 | Distribution: "ubuntu", 64 | Release: "artful", 65 | }, 66 | } 67 | 68 | image := image.NewIncusImage(context.TODO(), cacheDir, "", cacheDir, definition) 69 | 70 | err = os.MkdirAll(filepath.Join(cacheDir, "rootfs", "etc"), 0o755) 71 | require.NoError(t, err) 72 | 73 | createTestFile(t, filepath.Join(cacheDir, "rootfs", "etc", "hostname"), "hostname") 74 | 75 | err = generator.RunIncus(image, shared.DefinitionTargetIncus{}) 76 | require.NoError(t, err) 77 | 78 | validateTestFile(t, filepath.Join(cacheDir, "templates", "hostname.tpl"), "{{ container.name }}\n") 79 | } 80 | -------------------------------------------------------------------------------- /sources/testdata/key2.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | xsFNBFufwdoBEADv/Gxytx/LcSXYuM0MwKojbBye81s0G1nEx+lz6VAUpIUZnbkq 4 | dXBHC+dwrGS/CeeLuAjPRLU8AoxE/jjvZVp8xFGEWHYdklqXGZ/gJfP5d3fIUBtZ 5 | HZEJl8B8m9pMHf/AQQdsC+YzizSG5t5Mhnotw044LXtdEEkx2t6Jz0OGrh+5Ioxq 6 | X7pZiq6Cv19BohaUioKMdp7ES6RYfN7ol6HSLFlrMXtVfh/ijpN9j3ZhVGVeRC8k 7 | KHQsJ5PkIbmvxBiUh7SJmfZUx0IQhNMaDHXfdZAGNtnhzzNReb1FqNLSVkrS/Pns 8 | AQzMhG1BDm2VOSF64jebKXffFqM5LXRQTeqTLsjUbbrqR6s/GCO8UF7jfUj6I7ta 9 | LygmsHO/JD4jpKRC0gbpUBfaiJyLvuepx3kWoqL3sN0LhlMI80+fA7GTvoOx4tpq 10 | VlzlE6TajYu+jfW3QpOFS5ewEMdL26hzxsZg/geZvTbArcP+OsJKRmhv4kNo6Ayd 11 | yHQ/3ZV/f3X9mT3/SPLbJaumkgp3Yzd6t5PeBu+ZQk/mN5WNNuaihNEV7llb1Zhv 12 | Y0Fxu9BVd/BNl0rzuxp3rIinB2TX2SCg7wE5xXkwXuQ/2eTDE0v0HlGntkuZjGow 13 | DZkxHZQSxZVOzdZCRVaX/WEFLpKa2AQpw5RJrQ4oZ/OfifXyJzP27o03wQARAQAB 14 | zUJVYnVudHUgQXJjaGl2ZSBBdXRvbWF0aWMgU2lnbmluZyBLZXkgKDIwMTgpIDxm 15 | dHBtYXN0ZXJAdWJ1bnR1LmNvbT7CwXgEEwEKACIFAlufwdoCGwMGCwkIBwMCBhUI 16 | AgkKCwQWAgMBAh4BAheAAAoJEIcZINGZG8k8LHMQAKS2cnxz/5WaoCOWArf5g6UH 17 | beOCgc5DBm0hCuFDZWWv427aGei3CPuLw0DGLCXZdyc5dqE8mvjMlOmmAKKlj1uG 18 | g3TYCbQWjWPeMnBPZbkFgkZoXJ7/6CB7bWRht1sHzpt1LTZ+SYDwOwJ68QRp7DRa 19 | Zl9Y6QiUbeuhq2DUcTofVbBxbhrckN4ZteLvm+/nG9m/ciopc66LwRdkxqfJ32Cy 20 | q+1TS5VaIJDG7DWziG+Kbu6qCDM4QNlg3LH7p14CrRxAbc4lvohRgsV4eQqsIcdF 21 | kuVY5HPPj2K8TqpY6STe8Gh0aprG1RV8ZKay3KSMpnyV1fAKn4fM9byiLzQAovC0 22 | LZ9MMMsrAS/45AvC3IEKSShjLFn1X1dRCiO6/7jmZEoZtAp53hkf8SMBsi78hVNr 23 | BumZwfIdBA1v22+LY4xQK8q4XCoRcA9G+pvzU9YVW7cRnDZZGl0uwOw7z9PkQBF5 24 | KFKjWDz4fCk+K6+YtGpovGKekGBb8I7EA6UpvPgqA/QdI0t1IBP0N06RQcs1fUaA 25 | QEtz6DGy5zkRhR4pGSZn+dFET7PdAjEK84y7BdY4t+U1jcSIvBj0F2B7LwRL7xGp 26 | SpIKi/ekAXLs117bvFHaCvmUYN7JVp1GMmVFxhIdx6CFm3fxG8QjNb5tere/YqK+ 27 | uOgcXny1UlwtCUzlrSaPwsFzBBABCgAdFiEEFT8cnvE5X78ANS6NC/uEfz8nL1sF 28 | AlufxEMACgkQC/uEfz8nL1tuFw/9GgaeggvCn15QplABa86OReJARxnAxpaL223p 29 | LkgAbBYAOT7PmTjwwHCqGeJZGLzAQsGLc6WkQDegewQCMWLp+1zOHmUBHbZPsz3E 30 | 76Ac381FAXhZBj8MLbcyOROsKYKZ9M/yGerMpVx4B8WNb5P+t9ttAwwAR/lNs5OS 31 | 3lpV4nkwIzvxA6Wnq0gWKBL/9rc7sL+qWeJDnQEkq1Z/dNBbgIWktDtqeIXFldgj 32 | YOX+x1RN81beLVDtRLoOU0IkQsFGaOOb0o2x8/dmYM2cXuchNGYmdY2Z5jeLI1F0 33 | dzCR+CRUEDFdr0cF94USgVGWyCoaHdABTRD5e/uIEySL0T9ym93RNBtoc9gPENFB 34 | 2ASMJgkMNINiV82alPjYYrbs+ZVHuLQIgd+qw/N6zwLtVDgo2Pc6FXZpqmSjRRmt 35 | BRJuv+VnDBeAOstl0QloRm5gRBp/wgt93E1Ah+QJRVuMQFqz0nPZWTwfcGagmSEu 36 | rWiKX8n2FFYkiLfyUW0335TN88Z99+gvQ+AySAFu8ReT/lQzAPRPNRLjpAk5e1Fu 37 | MzQYoBJcYwP0sjAIO1AWmguPI1KLfnVnXnsT5JYMbG2DCLHI/OIvnpRq8v955glZ 38 | 5L9aq8bNnOwC2BK6MVUspbJRpGLQ29hbeH8jnRPOPQ+Sbwa2C8/ZSoBa/L6JGl5R 39 | DaOLQ1w= 40 | =VTg2 41 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /sources/rpmbootstrap.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | type rpmbootstrap struct { 12 | common 13 | } 14 | 15 | func (s *rpmbootstrap) yumordnf() (cmd string, err error) { 16 | // check whether yum or dnf command exists 17 | for _, cmd = range []string{"yum", "dnf"} { 18 | if err = shared.RunCommand(s.ctx, nil, nil, cmd, "--version"); err == nil { 19 | return cmd, err 20 | } 21 | } 22 | cmd = "" 23 | err = fmt.Errorf("Command yum or dnf not found, sudo apt-get install yum or sudo apt-get install dnf and try again") 24 | return cmd, err 25 | } 26 | 27 | func (s *rpmbootstrap) repodirs() (dir string, err error) { 28 | reposdir := path.Join(s.sourcesDir, "etc", "yum.repos.d") 29 | err = os.MkdirAll(reposdir, 0o755) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | distribution := s.definition.Image.Distribution 35 | content := s.definition.Source.URL 36 | if distribution == "" || content == "" { 37 | err = fmt.Errorf("No valid distribution and source url specified") 38 | return "", err 39 | } 40 | 41 | err = os.WriteFile(path.Join(reposdir, distribution+".repo"), []byte(content), 0o644) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | return reposdir, nil 47 | } 48 | 49 | // Run runs yum --installroot. 50 | func (s *rpmbootstrap) Run() (err error) { 51 | cmd, err := s.yumordnf() 52 | if err != nil { 53 | return err 54 | } 55 | 56 | repodir, err := s.repodirs() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | release := s.definition.Image.Release 62 | args := []string{ 63 | fmt.Sprintf("--installroot=%s", s.rootfsDir), 64 | fmt.Sprintf("--releasever=%s", release), 65 | fmt.Sprintf("--setopt=reposdir=%s", repodir), 66 | "install", "-y", 67 | } 68 | 69 | os.RemoveAll(s.rootfsDir) 70 | earlyPackagesRemove := s.definition.GetEarlyPackages("remove") 71 | 72 | for _, pkg := range earlyPackagesRemove { 73 | args = append(args, fmt.Sprintf("--exclude=%s", pkg)) 74 | } 75 | 76 | pkgs := []string{"yum", "dnf"} 77 | components := s.definition.Source.Components 78 | 79 | for _, pkg := range components { 80 | pkg, err = shared.RenderTemplate(pkg, s.definition) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | pkgs = append(pkgs, pkg) 86 | } 87 | 88 | earlyPackagesInstall := s.definition.GetEarlyPackages("install") 89 | pkgs = append(pkgs, earlyPackagesInstall...) 90 | args = append(args, pkgs...) 91 | 92 | // Install 93 | if err = shared.RunCommand(s.ctx, nil, nil, cmd, args...); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /.sphinx/_templates/footer.html: -------------------------------------------------------------------------------- 1 | {# ru-fu: copied from Furo, with modifications as stated below #} 2 | 3 | 31 |
32 |
33 | {%- if show_copyright %} 34 | 45 | {%- endif %} 46 | 47 | {# ru-fu: removed "Made with" #} 48 | 49 | {%- if last_updated -%} 50 |
51 | {% trans last_updated=last_updated|e -%} 52 | Last updated on {{ last_updated }} 53 | {%- endtrans -%} 54 |
55 | {%- endif %} 56 |
57 |
58 | 59 | {# ru-fu: replaced RTD icons with our links #} 60 | 61 | {%- if show_source and has_source and sourcename %} 62 |
63 | Show source 65 |
66 | {%- endif %} 67 | {% if github_url and github_version and github_folder and github_filetype and has_source and sourcename %} 68 |
69 | Edit on GitHub 70 |
71 | {% endif %} 72 | 73 |
74 |
75 | 76 | -------------------------------------------------------------------------------- /sources/funtoo-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "path/filepath" 8 | 9 | "github.com/lxc/distrobuilder/shared" 10 | ) 11 | 12 | type funtoo struct { 13 | common 14 | } 15 | 16 | // Run downloads a Funtoo stage3 tarball. 17 | func (s *funtoo) Run() error { 18 | topLevelArch := s.definition.Image.ArchitectureMapped 19 | 20 | switch topLevelArch { 21 | case "generic_32": 22 | topLevelArch = "x86-32bit" 23 | case "generic_64": 24 | topLevelArch = "x86-64bit" 25 | case "armv7a_vfpv3_hardfp": 26 | topLevelArch = "arm-32bit" 27 | case "arm64_generic": 28 | topLevelArch = "arm-64bit" 29 | } 30 | 31 | // Keep release backward compatible to old implementation 32 | // and to permit to have yet the funtoo/1.4 alias. 33 | if s.definition.Image.Release == "1.4" { 34 | s.definition.Image.Release = "1.4-release-std" 35 | } 36 | 37 | baseURL := fmt.Sprintf("%s/%s/%s/%s", 38 | s.definition.Source.URL, s.definition.Image.Release, 39 | topLevelArch, s.definition.Image.ArchitectureMapped) 40 | 41 | // Get the latest release tarball. 42 | fname := "stage3-latest.tar.xz" 43 | tarball := fmt.Sprintf("%s/%s", baseURL, fname) 44 | 45 | url, err := url.Parse(tarball) 46 | if err != nil { 47 | return fmt.Errorf("Failed to parse URL %q: %w", tarball, err) 48 | } 49 | 50 | if !s.definition.Source.SkipVerification && url.Scheme != "https" && 51 | len(s.definition.Source.Keys) == 0 { 52 | return errors.New("GPG keys are required if downloading from HTTP") 53 | } 54 | 55 | var fpath string 56 | 57 | fpath, err = s.DownloadHash(s.definition.Image, tarball, "", nil) 58 | if err != nil { 59 | return fmt.Errorf("Failed to download %q: %w", tarball, err) 60 | } 61 | 62 | // Force gpg checks when using http 63 | if !s.definition.Source.SkipVerification && url.Scheme != "https" { 64 | _, err = s.DownloadHash(s.definition.Image, tarball+".gpg", "", nil) 65 | if err != nil { 66 | return fmt.Errorf("Failed to download %q: %w", tarball+".gpg", err) 67 | } 68 | 69 | valid, err := s.VerifyFile( 70 | filepath.Join(fpath, fname), 71 | filepath.Join(fpath, fname+".gpg")) 72 | if err != nil { 73 | return fmt.Errorf("Failed to verify file: %w", err) 74 | } 75 | 76 | if !valid { 77 | return fmt.Errorf("Invalid signature for %q", filepath.Join(fpath, fname)) 78 | } 79 | } 80 | 81 | s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image") 82 | 83 | // Unpack 84 | err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir) 85 | if err != nil { 86 | return fmt.Errorf("Failed to unpack %q: %w", filepath.Join(fpath, fname), err) 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /generators/hosts_test.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/lxc/distrobuilder/image" 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | func TestHostsGeneratorRunLXC(t *testing.T) { 16 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 17 | require.NoError(t, err) 18 | 19 | rootfsDir := filepath.Join(cacheDir, "rootfs") 20 | 21 | setup(t, cacheDir) 22 | defer teardown(cacheDir) 23 | 24 | generator, err := Load("hosts", nil, cacheDir, rootfsDir, shared.DefinitionFile{Path: "/etc/hosts"}, shared.Definition{}) 25 | require.IsType(t, &hosts{}, generator) 26 | require.NoError(t, err) 27 | 28 | definition := shared.Definition{ 29 | Image: shared.DefinitionImage{ 30 | Distribution: "ubuntu", 31 | Release: "artful", 32 | }, 33 | } 34 | 35 | image := image.NewLXCImage(context.TODO(), cacheDir, "", cacheDir, definition) 36 | 37 | err = os.MkdirAll(filepath.Join(cacheDir, "rootfs", "etc"), 0o755) 38 | require.NoError(t, err) 39 | 40 | createTestFile(t, filepath.Join(cacheDir, "rootfs", "etc", "hosts"), 41 | "127.0.0.1\tlocalhost\n127.0.0.1\tdistrobuilder\n") 42 | 43 | err = generator.RunLXC(image, shared.DefinitionTargetLXC{}) 44 | require.NoError(t, err) 45 | 46 | validateTestFile(t, filepath.Join(cacheDir, "rootfs", "etc", "hosts"), 47 | "127.0.0.1\tlocalhost\n127.0.0.1\tLXC_NAME\n") 48 | } 49 | 50 | func TestHostsGeneratorRunIncus(t *testing.T) { 51 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 52 | require.NoError(t, err) 53 | 54 | rootfsDir := filepath.Join(cacheDir, "rootfs") 55 | 56 | setup(t, cacheDir) 57 | defer teardown(cacheDir) 58 | 59 | generator, err := Load("hosts", nil, cacheDir, rootfsDir, shared.DefinitionFile{Path: "/etc/hosts"}, shared.Definition{}) 60 | require.IsType(t, &hosts{}, generator) 61 | require.NoError(t, err) 62 | 63 | definition := shared.Definition{ 64 | Image: shared.DefinitionImage{ 65 | Distribution: "ubuntu", 66 | Release: "artful", 67 | }, 68 | } 69 | 70 | image := image.NewIncusImage(context.TODO(), cacheDir, "", cacheDir, definition) 71 | 72 | err = os.MkdirAll(filepath.Join(cacheDir, "rootfs", "etc"), 0o755) 73 | require.NoError(t, err) 74 | 75 | createTestFile(t, filepath.Join(cacheDir, "rootfs", "etc", "hosts"), 76 | "127.0.0.1\tlocalhost\n127.0.0.1\tdistrobuilder\n") 77 | 78 | err = generator.RunIncus(image, shared.DefinitionTargetIncus{}) 79 | require.NoError(t, err) 80 | 81 | validateTestFile(t, filepath.Join(cacheDir, "templates", "hosts.tpl"), 82 | "127.0.0.1\tlocalhost\n127.0.0.1\t{{ container.name }}\n") 83 | } 84 | -------------------------------------------------------------------------------- /generators/hostname.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/lxc/incus/v6/shared/api" 9 | incus "github.com/lxc/incus/v6/shared/util" 10 | 11 | "github.com/lxc/distrobuilder/image" 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | type hostname struct { 16 | common 17 | } 18 | 19 | // RunLXC creates a hostname template. 20 | func (g *hostname) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error { 21 | // Skip if the file doesn't exist 22 | if !incus.PathExists(filepath.Join(g.sourceDir, g.defFile.Path)) { 23 | return nil 24 | } 25 | 26 | // Create new hostname file 27 | file, err := os.Create(filepath.Join(g.sourceDir, g.defFile.Path)) 28 | if err != nil { 29 | return fmt.Errorf("Failed to create file %q: %w", filepath.Join(g.sourceDir, g.defFile.Path), err) 30 | } 31 | 32 | defer file.Close() 33 | 34 | // Write LXC specific string to the hostname file 35 | _, err = file.WriteString("LXC_NAME\n") 36 | if err != nil { 37 | return fmt.Errorf("Failed to write to hostname file: %w", err) 38 | } 39 | 40 | // Add hostname path to LXC's templates file 41 | err = img.AddTemplate(g.defFile.Path) 42 | if err != nil { 43 | return fmt.Errorf("Failed to add template: %w", err) 44 | } 45 | 46 | return nil 47 | } 48 | 49 | // RunIncus creates a hostname template. 50 | func (g *hostname) RunIncus(img *image.IncusImage, target shared.DefinitionTargetIncus) error { 51 | // Skip if the file doesn't exist 52 | if !incus.PathExists(filepath.Join(g.sourceDir, g.defFile.Path)) { 53 | return nil 54 | } 55 | 56 | templateDir := filepath.Join(g.cacheDir, "templates") 57 | 58 | err := os.MkdirAll(templateDir, 0o755) 59 | if err != nil { 60 | return fmt.Errorf("Failed to create directory %q: %w", templateDir, err) 61 | } 62 | 63 | file, err := os.Create(filepath.Join(templateDir, "hostname.tpl")) 64 | if err != nil { 65 | return fmt.Errorf("Failed to create file %q: %w", filepath.Join(templateDir, "hostname.tpl"), err) 66 | } 67 | 68 | defer file.Close() 69 | 70 | _, err = file.WriteString("{{ container.name }}\n") 71 | if err != nil { 72 | return fmt.Errorf("Failed to write to hostname file: %w", err) 73 | } 74 | 75 | // Add to Incus templates 76 | img.Metadata.Templates[g.defFile.Path] = &api.ImageMetadataTemplate{ 77 | Template: "hostname.tpl", 78 | Properties: g.defFile.Template.Properties, 79 | When: g.defFile.Template.When, 80 | } 81 | 82 | if len(g.defFile.Template.When) == 0 { 83 | img.Metadata.Templates[g.defFile.Path].When = []string{ 84 | "create", 85 | "copy", 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // Run does nothing. 93 | func (g *hostname) Run() error { 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /managers/common.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/lxc/distrobuilder/shared" 9 | ) 10 | 11 | type common struct { 12 | commands managerCommands 13 | flags managerFlags 14 | hooks managerHooks 15 | logger *logrus.Logger 16 | definition shared.Definition 17 | ctx context.Context 18 | } 19 | 20 | func (c *common) init(ctx context.Context, logger *logrus.Logger, definition shared.Definition) { 21 | c.logger = logger 22 | c.definition = definition 23 | c.ctx = ctx 24 | } 25 | 26 | // Install installs packages to the rootfs. 27 | func (c *common) install(pkgs, flags []string) error { 28 | if len(c.flags.install) == 0 || pkgs == nil || len(pkgs) == 0 { 29 | return nil 30 | } 31 | 32 | args := append(c.flags.global, c.flags.install...) 33 | args = append(args, flags...) 34 | args = append(args, pkgs...) 35 | 36 | return shared.RunCommand(c.ctx, nil, nil, c.commands.install, args...) 37 | } 38 | 39 | // Remove removes packages from the rootfs. 40 | func (c *common) remove(pkgs, flags []string) error { 41 | if len(c.flags.remove) == 0 || pkgs == nil || len(pkgs) == 0 { 42 | return nil 43 | } 44 | 45 | args := append(c.flags.global, c.flags.remove...) 46 | args = append(args, flags...) 47 | args = append(args, pkgs...) 48 | 49 | return shared.RunCommand(c.ctx, nil, nil, c.commands.remove, args...) 50 | } 51 | 52 | // Clean cleans up cached files used by the package managers. 53 | func (c *common) clean() error { 54 | var err error 55 | 56 | if len(c.flags.clean) == 0 { 57 | return nil 58 | } 59 | 60 | args := append(c.flags.global, c.flags.clean...) 61 | 62 | err = shared.RunCommand(c.ctx, nil, nil, c.commands.clean, args...) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if c.hooks.clean != nil { 68 | err = c.hooks.clean() 69 | } 70 | 71 | return err 72 | } 73 | 74 | // Refresh refreshes the local package database. 75 | func (c *common) refresh() error { 76 | if len(c.flags.refresh) == 0 { 77 | return nil 78 | } 79 | 80 | if c.hooks.preRefresh != nil { 81 | err := c.hooks.preRefresh() 82 | if err != nil { 83 | return err 84 | } 85 | } 86 | 87 | args := append(c.flags.global, c.flags.refresh...) 88 | 89 | return shared.RunCommand(c.ctx, nil, nil, c.commands.refresh, args...) 90 | } 91 | 92 | // Update updates all packages. 93 | func (c *common) update() error { 94 | if len(c.flags.update) == 0 { 95 | return nil 96 | } 97 | 98 | args := append(c.flags.global, c.flags.update...) 99 | 100 | return shared.RunCommand(c.ctx, nil, nil, c.commands.update, args...) 101 | } 102 | 103 | func (c *common) manageRepository(repo shared.DefinitionPackagesRepository) error { 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /shared/archive_linux.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "github.com/lxc/incus/v6/shared/archive" 12 | "github.com/lxc/incus/v6/shared/subprocess" 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | // Unpack unpacks a tarball. 17 | func Unpack(file string, path string) error { 18 | extractArgs, extension, _, err := archive.DetectCompression(file) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | command := "" 24 | args := []string{} 25 | var reader io.Reader 26 | if strings.HasPrefix(extension, ".tar") { 27 | command = "tar" 28 | args = append(args, "--restrict", "--force-local") 29 | args = append(args, "-C", path, "--numeric-owner", "--xattrs-include=*") 30 | args = append(args, extractArgs...) 31 | args = append(args, "-") 32 | 33 | f, err := os.Open(file) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | defer f.Close() 39 | 40 | reader = f 41 | } else if strings.HasPrefix(extension, ".squashfs") { 42 | // unsquashfs does not support reading from stdin, 43 | // so ProgressTracker is not possible. 44 | command = "unsquashfs" 45 | args = append(args, "-f", "-d", path, "-n", file) 46 | } else { 47 | return fmt.Errorf("Unsupported image format: %s", extension) 48 | } 49 | 50 | err = subprocess.RunCommandWithFds(context.TODO(), reader, nil, command, args...) 51 | if err != nil { 52 | // We can't create char/block devices in unpriv containers so ignore related errors. 53 | if command == "unsquashfs" { 54 | var runError *subprocess.RunError 55 | 56 | ok := errors.As(err, &runError) 57 | if !ok || runError.StdErr().String() == "" { 58 | return err 59 | } 60 | 61 | // Confirm that all errors are related to character or block devices. 62 | found := false 63 | for _, line := range strings.Split(runError.StdErr().String(), "\n") { 64 | line = strings.TrimSpace(line) 65 | if line == "" { 66 | continue 67 | } 68 | 69 | if !strings.Contains(line, "failed to create block device") { 70 | continue 71 | } 72 | 73 | if !strings.Contains(line, "failed to create character device") { 74 | continue 75 | } 76 | 77 | // We found an actual error. 78 | found = true 79 | } 80 | 81 | if !found { 82 | // All good, assume everything unpacked. 83 | return nil 84 | } 85 | } 86 | 87 | // Check if we ran out of space 88 | fs := unix.Statfs_t{} 89 | 90 | err1 := unix.Statfs(path, &fs) 91 | if err1 != nil { 92 | return err1 93 | } 94 | 95 | // Check if we're running out of space 96 | if int64(fs.Bfree) < 10 { 97 | return fmt.Errorf("Unable to unpack image, run out of disk space") 98 | } 99 | 100 | return fmt.Errorf("Unpack failed: %w", err) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pull requests: 4 | 5 | Changes to this project should be proposed as pull requests on Github 6 | at: 7 | 8 | Proposed changes will then go through code review there and once acked, 9 | be merged in the main branch. 10 | 11 | 12 | ## License and copyright: 13 | 14 | By default, any contribution to this project is made under the Apache 15 | 2.0 license. 16 | 17 | The author of a change remains the copyright holder of their code 18 | (no copyright assignment). 19 | 20 | 21 | ## Developer Certificate of Origin: 22 | 23 | To improve tracking of contributions to this project we use the DCO 1.1 24 | and use a "sign-off" procedure for all changes going into the branch. 25 | 26 | The sign-off is a simple line at the end of the explanation for the 27 | commit which certifies that you wrote it or otherwise have the right 28 | to pass it on as an open-source contribution. 29 | 30 | > Developer Certificate of Origin 31 | > Version 1.1 32 | > 33 | > Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 34 | > 660 York Street, Suite 102, 35 | > San Francisco, CA 94110 USA 36 | > 37 | > Everyone is permitted to copy and distribute verbatim copies of this 38 | > license document, but changing it is not allowed. 39 | > 40 | > Developer's Certificate of Origin 1.1 41 | > 42 | > By making a contribution to this project, I certify that: 43 | > 44 | > (a) The contribution was created in whole or in part by me and I 45 | > have the right to submit it under the open source license 46 | > indicated in the file; or 47 | > 48 | > (b) The contribution is based upon previous work that, to the best 49 | > of my knowledge, is covered under an appropriate open source 50 | > license and I have the right under that license to submit that 51 | > work with modifications, whether created in whole or in part 52 | > by me, under the same open source license (unless I am 53 | > permitted to submit under a different license), as indicated 54 | > in the file; or 55 | > 56 | > (c) The contribution was provided directly to me by some other 57 | > person who certified (a), (b) or (c) and I have not modified 58 | > it. 59 | > 60 | > (d) I understand and agree that this project and the contribution 61 | > are public and that a record of the contribution (including all 62 | > personal information I submit with it, including my sign-off) is 63 | > maintained indefinitely and may be redistributed consistent with 64 | > this project or the open source license(s) involved. 65 | 66 | An example of a valid sign-off line is: 67 | 68 | ``` 69 | Signed-off-by: Random J Developer 70 | ``` 71 | 72 | Use your real name and a valid e-mail address. 73 | Sorry, no pseudonyms or anonymous contributions are allowed. 74 | 75 | We also require each commit be individually signed-off by their author, 76 | even when part of a larger set. You may find `git commit -s` useful. 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell grep "var Version" shared/version/version.go | cut -d'"' -f2) 2 | ARCHIVE=distrobuilder-$(VERSION).tar 3 | GO111MODULE=on 4 | SPHINXENV=.sphinx/venv/bin/activate 5 | GOPATH=$(shell go env GOPATH) 6 | export GOFLAGS=-tags=containers_image_storage_stub,containers_image_docker_daemon_stub,containers_image_openpgp 7 | 8 | .PHONY: default 9 | default: 10 | gofmt -s -w . 11 | go install -v ./... 12 | @echo "distrobuilder built successfully" 13 | 14 | .PHONY: update-gomod 15 | update-gomod: 16 | go get -t -v -u ./... 17 | go mod tidy --go=1.25.0 18 | go get toolchain@none 19 | @echo "Dependencies updated" 20 | 21 | .PHONY: check 22 | check: default 23 | sudo GOENV=$(shell go env GOENV) GOFLAGS=$(GOFLAGS) go test -v ./... 24 | 25 | .PHONY: dist 26 | dist: 27 | # Cleanup 28 | rm -Rf $(ARCHIVE).gz 29 | 30 | # Create build dir 31 | $(eval TMP := $(shell mktemp -d)) 32 | git archive --prefix=distrobuilder-$(VERSION)/ HEAD | tar -x -C $(TMP) 33 | mkdir -p $(TMP)/_dist/src/github.com/lxc 34 | ln -s ../../../../distrobuilder-$(VERSION) $(TMP)/_dist/src/github.com/lxc/distrobuilder 35 | 36 | # Download dependencies 37 | cd $(TMP)/distrobuilder-$(VERSION) && go mod vendor 38 | 39 | # Assemble tarball 40 | tar --exclude-vcs -C $(TMP) -zcf $(ARCHIVE).gz distrobuilder-$(VERSION)/ 41 | 42 | # Cleanup 43 | rm -Rf $(TMP) 44 | 45 | .PHONY: doc-setup 46 | doc-setup: 47 | @echo "Setting up documentation build environment" 48 | python3 -m venv .sphinx/venv 49 | . $(SPHINXENV) ; pip install --upgrade -r .sphinx/requirements.txt 50 | mkdir -p .sphinx/deps/ .sphinx/themes/ 51 | wget -N -P .sphinx/_static/download https://linuxcontainers.org/static/img/favicon.ico https://linuxcontainers.org/static/img/containers.png https://linuxcontainers.org/static/img/containers.small.png 52 | rm -Rf doc/html 53 | 54 | .PHONY: doc 55 | doc: doc-setup doc-incremental 56 | 57 | .PHONY: doc-incremental 58 | doc-incremental: 59 | @echo "Build the documentation" 60 | . $(SPHINXENV) ; sphinx-build -c .sphinx/ -b dirhtml doc/ doc/html/ -w .sphinx/warnings.txt 61 | 62 | .PHONY: doc-serve 63 | doc-serve: 64 | cd doc/html; python3 -m http.server 8001 65 | 66 | .PHONY: doc-spellcheck 67 | doc-spellcheck: doc 68 | . $(SPHINXENV) ; python3 -m pyspelling -c .sphinx/.spellcheck.yaml 69 | 70 | .PHONY: doc-linkcheck 71 | doc-linkcheck: doc-setup 72 | . $(SPHINXENV) ; sphinx-build -c .sphinx/ -b linkcheck doc/ doc/html/ 73 | 74 | .PHONY: doc-lint 75 | doc-lint: 76 | .sphinx/.markdownlint/doc-lint.sh 77 | 78 | .PHONY: static-analysis 79 | static-analysis: 80 | ifeq ($(shell command -v golangci-lint),) 81 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin 82 | endif 83 | ifeq ($(shell command -v codespell),) 84 | echo "Please install codespell" 85 | exit 1 86 | endif 87 | $(GOPATH)/bin/golangci-lint run --timeout 5m 88 | run-parts $(shell run-parts -V 2> /dev/null 1> /dev/null && echo -n "--exit-on-error --regex '.sh'") test/lint 89 | -------------------------------------------------------------------------------- /managers/yum.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | incus "github.com/lxc/incus/v6/shared/util" 12 | 13 | "github.com/lxc/distrobuilder/shared" 14 | ) 15 | 16 | type yum struct { 17 | common 18 | } 19 | 20 | func (m *yum) load() error { 21 | m.commands = managerCommands{ 22 | clean: "yum", 23 | install: "yum", 24 | refresh: "yum", 25 | remove: "yum", 26 | update: "yum", 27 | } 28 | 29 | m.flags = managerFlags{ 30 | clean: []string{ 31 | "clean", "all", 32 | }, 33 | global: []string{ 34 | "-y", 35 | }, 36 | install: []string{ 37 | "install", 38 | }, 39 | remove: []string{ 40 | "remove", 41 | }, 42 | refresh: []string{ 43 | "makecache", 44 | }, 45 | update: []string{ 46 | "update", 47 | }, 48 | } 49 | 50 | var buf bytes.Buffer 51 | 52 | err := shared.RunCommand(m.ctx, nil, &buf, "yum", "--help") 53 | if err != nil { 54 | return fmt.Errorf("Failed running yum: %w", err) 55 | } 56 | 57 | scanner := bufio.NewScanner(&buf) 58 | 59 | for scanner.Scan() { 60 | if strings.Contains(scanner.Text(), "--allowerasing") { 61 | m.flags.global = append(m.flags.global, "--allowerasing") 62 | continue 63 | } 64 | 65 | if strings.Contains(scanner.Text(), "--nobest") { 66 | m.flags.update = append(m.flags.update, "--nobest") 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (m *yum) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 74 | // Run rpmdb --rebuilddb 75 | err := shared.RunCommand(m.ctx, nil, nil, "rpmdb", "--rebuilddb") 76 | if err != nil { 77 | return fmt.Errorf("failed to run rpmdb --rebuilddb: %w", err) 78 | } 79 | 80 | return yumManageRepository(repoAction) 81 | } 82 | 83 | func yumManageRepository(repoAction shared.DefinitionPackagesRepository) error { 84 | targetFile := filepath.Join("/etc/yum.repos.d", repoAction.Name) 85 | 86 | if !strings.HasSuffix(targetFile, ".repo") { 87 | targetFile = fmt.Sprintf("%s.repo", targetFile) 88 | } 89 | 90 | if !incus.PathExists(filepath.Dir(targetFile)) { 91 | err := os.MkdirAll(filepath.Dir(targetFile), 0o755) 92 | if err != nil { 93 | return fmt.Errorf("Failed to create directory %q: %w", filepath.Dir(targetFile), err) 94 | } 95 | } 96 | 97 | f, err := os.Create(targetFile) 98 | if err != nil { 99 | return fmt.Errorf("Failed to create file %q: %w", targetFile, err) 100 | } 101 | 102 | defer f.Close() 103 | 104 | _, err = f.WriteString(repoAction.URL) 105 | if err != nil { 106 | return fmt.Errorf("Failed to write to file %q: %w", targetFile, err) 107 | } 108 | 109 | // Append final new line if missing 110 | if !strings.HasSuffix(repoAction.URL, "\n") { 111 | _, err = f.WriteString("\n") 112 | if err != nil { 113 | return fmt.Errorf("Failed to write to file %q: %w", targetFile, err) 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /doc/reference/source.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | In order to create an image, a source must be defined. 4 | The source section is defined as follows: 5 | 6 | ```yaml 7 | source: 8 | downloader: # required 9 | url: 10 | keys: 11 | keyserver: 12 | variant: 13 | suite: 14 | same_as: 15 | skip_verification: 16 | components: 17 | ``` 18 | 19 | The `downloader` field defines a downloader which pulls a rootfs image which will be used as a starting point. 20 | It needs to be one of 21 | 22 | * `alpaquita-http` 23 | * `alpinelinux-http` 24 | * `alt-http` 25 | * `apertis-http` 26 | * `archlinux-http` 27 | * `centos-http` 28 | * `debootstrap` 29 | * `docker-http` 30 | * `fedora-http` 31 | * `funtoo-http` 32 | * `gentoo-http` 33 | * `nixos-http` 34 | * `openeuler-http` 35 | * `opensuse-http` 36 | * `openwrt-http` 37 | * `oraclelinux-http` 38 | * `sabayon-http` 39 | * `rootfs-http` 40 | * `ubuntu-http` 41 | * `voidlinux-http` 42 | 43 | The `url` field defines the URL or mirror of the rootfs image. 44 | Although this field is not required, most downloaders will need it. The `rootfs-http` downloader also supports local image files when prefixed with `file://`, e.g. `url: file:///home/user/image.tar.gz` or `url: file:///home/user/image.squashfs`. 45 | 46 | The `keys` field is a list of GPG keys. 47 | These keys can be listed as fingerprints or armored keys. 48 | The latter has the advantage of not having to rely on a key server to download the key from. 49 | The keys are used to verify the downloaded rootfs tarball if downloaded from a insecure source (HTTP). 50 | 51 | The `keyserver` defaults to `hkps.pool.sks-keyservers.net` if none is provided. 52 | 53 | The `variant` field is only used in a few distributions and defaults to `default`. 54 | Here's a list downloaders and their possible variants: 55 | 56 | * `alpaquita-http`: `musl`, `glibc` 57 | * `centos-http`: `minimal`, `netinstall`, `LiveDVD` 58 | * `debootstrap`: `default`, `minbase`, `buildd`, `fakechroot` 59 | * `ubuntu-http`: `default`, `core` 60 | * `voidlinux-http`: `default`, `musl` 61 | 62 | All other downloaders ignore this field. 63 | 64 | The `suite` field is only used by the `debootstrap` downloader. 65 | If set, `debootstrap` will use `suite` instead of `image.release` as its first positional argument. 66 | 67 | If the `same_as` field is set, distrobuilder creates a temporary symlink in `/usr/share/debootstrap/scripts` which points to the `same_as` file inside that directory. 68 | This can be used if you want to run `debootstrap foo` but `foo` is missing due to `debootstrap` not being up-to-date. 69 | 70 | If `skip_verification` is true, the source tarball is not verified. 71 | 72 | If the `components` field is set, `debootstrap` will use packages from the listed components. 73 | 74 | If a package set has the `early` flag enabled, that list of packages will be installed 75 | while the source is being downloaded. (Note that `early` packages are only supported by 76 | the `debootstrap` downloader.) 77 | -------------------------------------------------------------------------------- /sources/source.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/lxc/distrobuilder/shared" 10 | ) 11 | 12 | // ErrUnknownDownloader represents the unknown downloader error. 13 | var ErrUnknownDownloader = errors.New("Unknown downloader") 14 | 15 | type downloader interface { 16 | init(ctx context.Context, logger *logrus.Logger, definition shared.Definition, rootfsDir string, cacheDir string, sourcesDir string) 17 | 18 | Downloader 19 | } 20 | 21 | // Downloader represents a source downloader. 22 | type Downloader interface { 23 | Run() error 24 | } 25 | 26 | var downloaders = map[string]func() downloader{ 27 | "almalinux-http": func() downloader { return &almalinux{} }, 28 | "alpaquita-http": func() downloader { return &alpaquita{} }, 29 | "alpinelinux-http": func() downloader { return &alpineLinux{} }, 30 | "alt-http": func() downloader { return &altLinux{} }, 31 | "apertis-http": func() downloader { return &apertis{} }, 32 | "archlinux-http": func() downloader { return &archlinux{} }, 33 | "busybox": func() downloader { return &busybox{} }, 34 | "centos-http": func() downloader { return ¢OS{} }, 35 | "debootstrap": func() downloader { return &debootstrap{} }, 36 | "docker-http": func() downloader { return &docker{} }, 37 | "fedora-http": func() downloader { return &fedora{} }, 38 | "funtoo-http": func() downloader { return &funtoo{} }, 39 | "gentoo-http": func() downloader { return &gentoo{} }, 40 | "nixos-http": func() downloader { return &nixos{} }, 41 | "openeuler-http": func() downloader { return &openEuler{} }, 42 | "opensuse-http": func() downloader { return &opensuse{} }, 43 | "openwrt-http": func() downloader { return &openwrt{} }, 44 | "oraclelinux-http": func() downloader { return &oraclelinux{} }, 45 | "plamolinux-http": func() downloader { return &plamolinux{} }, 46 | "rockylinux-http": func() downloader { return &rockylinux{} }, 47 | "rootfs-http": func() downloader { return &rootfs{} }, 48 | "rpmbootstrap": func() downloader { return &rpmbootstrap{} }, 49 | "springdalelinux-http": func() downloader { return &springdalelinux{} }, 50 | "ubuntu-http": func() downloader { return &ubuntu{} }, 51 | "voidlinux-http": func() downloader { return &voidlinux{} }, 52 | "vyos-http": func() downloader { return &vyos{} }, 53 | "slackware-http": func() downloader { return &slackware{} }, 54 | } 55 | 56 | // Load loads and initializes a downloader. 57 | func Load(ctx context.Context, downloaderName string, logger *logrus.Logger, definition shared.Definition, rootfsDir string, cacheDir string, sourcesDir string) (Downloader, error) { 58 | df, ok := downloaders[downloaderName] 59 | if !ok { 60 | return nil, ErrUnknownDownloader 61 | } 62 | 63 | d := df() 64 | 65 | d.init(ctx, logger, definition, rootfsDir, cacheDir, sourcesDir) 66 | 67 | return d, nil 68 | } 69 | -------------------------------------------------------------------------------- /distrobuilder/main_build-dir.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/lxc/distrobuilder/generators" 11 | "github.com/lxc/distrobuilder/shared" 12 | ) 13 | 14 | type cmdBuildDir struct { 15 | cmdBuild *cobra.Command 16 | global *cmdGlobal 17 | 18 | flagWithPostFiles bool 19 | } 20 | 21 | func (c *cmdBuildDir) command() *cobra.Command { 22 | c.cmdBuild = &cobra.Command{ 23 | Use: "build-dir ", 24 | Short: "Build plain rootfs", 25 | Args: cobra.ExactArgs(2), 26 | RunE: c.global.preRunBuild, 27 | PostRunE: func(cmd *cobra.Command, args []string) error { 28 | // Run global generators 29 | for _, file := range c.global.definition.Files { 30 | if !shared.ApplyFilter(&file, c.global.definition.Image.Release, c.global.definition.Image.ArchitectureMapped, c.global.definition.Image.Variant, c.global.definition.Targets.Type, 0) { 31 | continue 32 | } 33 | 34 | generator, err := generators.Load(file.Generator, c.global.logger, c.global.flagCacheDir, c.global.targetDir, file, *c.global.definition) 35 | if err != nil { 36 | return fmt.Errorf("Failed to load generator %q: %w", file.Generator, err) 37 | } 38 | 39 | c.global.logger.WithField("generator", file.Generator).Info("Running generator") 40 | 41 | err = generator.Run() 42 | if err != nil { 43 | continue 44 | } 45 | } 46 | 47 | if !c.flagWithPostFiles { 48 | return nil 49 | } 50 | 51 | exitChroot, err := shared.SetupChroot(c.global.targetDir, 52 | *c.global.definition, nil) 53 | if err != nil { 54 | return fmt.Errorf("Failed to setup chroot in %q: %w", c.global.targetDir, err) 55 | } 56 | 57 | c.global.logger.WithField("trigger", "post-files").Info("Running hooks") 58 | 59 | // Run post files hook 60 | for _, action := range c.global.definition.GetRunnableActions("post-files", shared.ImageTargetUndefined) { 61 | if action.Pongo { 62 | action.Action, err = shared.RenderTemplate(action.Action, c.global.definition) 63 | if err != nil { 64 | return fmt.Errorf("Failed to render action: %w", err) 65 | } 66 | } 67 | 68 | err := shared.RunScript(c.global.ctx, action.Action) 69 | if err != nil { 70 | { 71 | err := exitChroot() 72 | if err != nil { 73 | c.global.logger.WithField("err", err).Warn("Failed exiting chroot") 74 | } 75 | } 76 | 77 | return fmt.Errorf("Failed to run post-files: %w", err) 78 | } 79 | } 80 | 81 | err = exitChroot() 82 | if err != nil { 83 | return fmt.Errorf("Failed exiting chroot: %w", err) 84 | } 85 | 86 | return nil 87 | }, 88 | } 89 | 90 | c.cmdBuild.Flags().StringVar(&c.global.flagSourcesDir, "sources-dir", filepath.Join(os.TempDir(), "distrobuilder"), "Sources directory for distribution tarballs"+"``") 91 | c.cmdBuild.Flags().BoolVar(&c.global.flagKeepSources, "keep-sources", true, "Keep sources after build"+"``") 92 | c.cmdBuild.Flags().BoolVar(&c.flagWithPostFiles, "with-post-files", false, "Run post-files actions"+"``") 93 | return c.cmdBuild 94 | } 95 | -------------------------------------------------------------------------------- /sources/busybox.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/sha256" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/lxc/distrobuilder/shared" 12 | ) 13 | 14 | type busybox struct { 15 | common 16 | } 17 | 18 | // Run downloads a busybox tarball. 19 | func (s *busybox) Run() error { 20 | fname := fmt.Sprintf("busybox-%s.tar.bz2", s.definition.Image.Release) 21 | tarball := fmt.Sprintf("%s/%s", s.definition.Source.URL, fname) 22 | 23 | var ( 24 | fpath string 25 | err error 26 | ) 27 | 28 | if s.definition.Source.SkipVerification { 29 | fpath, err = s.DownloadHash(s.definition.Image, tarball, "", nil) 30 | } else { 31 | fpath, err = s.DownloadHash(s.definition.Image, tarball, tarball+".sha256", sha256.New()) 32 | } 33 | 34 | if err != nil { 35 | return fmt.Errorf("Failed to download %q: %w", tarball, err) 36 | } 37 | 38 | sourceDir := filepath.Join(s.cacheDir, "src") 39 | 40 | err = os.MkdirAll(sourceDir, 0o755) 41 | if err != nil { 42 | return fmt.Errorf("Failed to create directory %q: %w", sourceDir, err) 43 | } 44 | 45 | s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image") 46 | 47 | // Unpack 48 | err = shared.Unpack(filepath.Join(fpath, fname), sourceDir) 49 | if err != nil { 50 | return fmt.Errorf("Failed to unpack %q: %w", fname, err) 51 | } 52 | 53 | sourceDir = filepath.Join(sourceDir, fmt.Sprintf("busybox-%s", s.definition.Image.Release)) 54 | 55 | err = shared.RunScript(s.ctx, fmt.Sprintf(`#!/bin/sh 56 | set -eux 57 | 58 | source_dir=%s 59 | rootfs_dir=%s 60 | 61 | cwd="$(pwd)" 62 | 63 | cd "${source_dir}" 64 | make defconfig 65 | sed -ri 's/# CONFIG_STATIC .*/CONFIG_STATIC=y/g' .config 66 | make 67 | 68 | cd "${cwd}" 69 | mkdir -p "${rootfs_dir}/bin" 70 | mv ${source_dir}/busybox "${rootfs_dir}/bin/busybox" 71 | `, sourceDir, s.rootfsDir)) 72 | if err != nil { 73 | return fmt.Errorf("Failed to build busybox: %w", err) 74 | } 75 | 76 | var buf bytes.Buffer 77 | 78 | err = shared.RunCommand(s.ctx, os.Stdin, &buf, filepath.Join(s.rootfsDir, "bin", "busybox"), "--list-full") 79 | if err != nil { 80 | return fmt.Errorf("Failed to install busybox: %w", err) 81 | } 82 | 83 | scanner := bufio.NewScanner(&buf) 84 | 85 | for scanner.Scan() { 86 | path := filepath.Join(s.rootfsDir, scanner.Text()) 87 | 88 | if path == "" || path == "bin/busybox" { 89 | continue 90 | } 91 | 92 | s.logger.Debugf("Creating directory %q", path) 93 | 94 | err = os.MkdirAll(filepath.Dir(path), 0o755) 95 | if err != nil { 96 | return fmt.Errorf("Failed to create directory %q: %w", filepath.Dir(path), err) 97 | } 98 | 99 | s.logger.Debugf("Creating symlink %q -> %q", path, "/bin/busybox") 100 | 101 | err = os.Symlink("/bin/busybox", path) 102 | if err != nil { 103 | return fmt.Errorf("Failed to create symlink %q -> /bin/busybox: %w", path, err) 104 | } 105 | } 106 | 107 | for _, path := range []string{"dev", "mnt", "proc", "root", "sys", "tmp"} { 108 | err := os.Mkdir(filepath.Join(s.rootfsDir, path), 0o755) 109 | if err != nil { 110 | return fmt.Errorf("Failed to create directory %q: %w", filepath.Join(s.rootfsDir, path), err) 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /sources/debootstrap.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "slices" 9 | "strings" 10 | 11 | incus "github.com/lxc/incus/v6/shared/util" 12 | 13 | "github.com/lxc/distrobuilder/shared" 14 | ) 15 | 16 | type debootstrap struct { 17 | common 18 | } 19 | 20 | // Run runs debootstrap. 21 | func (s *debootstrap) Run() error { 22 | var args []string 23 | 24 | os.RemoveAll(s.rootfsDir) 25 | 26 | if s.definition.Source.Variant != "" { 27 | args = append(args, "--variant", s.definition.Source.Variant) 28 | } 29 | 30 | if s.definition.Image.ArchitectureMapped != "" { 31 | args = append(args, "--arch", s.definition.Image.ArchitectureMapped) 32 | } 33 | 34 | if s.definition.Source.SkipVerification { 35 | args = append(args, "--no-check-gpg") 36 | } 37 | 38 | if s.definition.Image.Distribution == "devuan" && slices.Contains([]string{"beowulf", "chimaera"}, s.definition.Image.Release) { 39 | // Workaround for debootstrap attempting to fetch non-existent usr-is-merged. 40 | args = append(args, "--exclude=usr-is-merged") 41 | } 42 | 43 | earlyPackagesInstall := s.definition.GetEarlyPackages("install") 44 | earlyPackagesRemove := s.definition.GetEarlyPackages("remove") 45 | 46 | if len(earlyPackagesInstall) > 0 { 47 | args = append(args, fmt.Sprintf("--include=%s", strings.Join(earlyPackagesInstall, ","))) 48 | } 49 | 50 | if len(earlyPackagesRemove) > 0 { 51 | args = append(args, fmt.Sprintf("--exclude=%s", strings.Join(earlyPackagesRemove, ","))) 52 | } 53 | 54 | if len(s.definition.Source.Components) > 0 { 55 | args = append(args, fmt.Sprintf("--components=%s", strings.Join(s.definition.Source.Components, ","))) 56 | } 57 | 58 | if len(s.definition.Source.Keys) > 0 { 59 | keyring, err := s.CreateGPGKeyring() 60 | if err != nil { 61 | return fmt.Errorf("Failed to create GPG keyring: %w", err) 62 | } 63 | 64 | defer os.RemoveAll(path.Dir(keyring)) 65 | 66 | args = append(args, "--keyring", keyring) 67 | } 68 | 69 | // If source.suite is set, debootstrap will use this instead of 70 | // image.release as its first positional argument (SUITE). This is important 71 | // for derivatives which don't have their own sources, e.g. Linux Mint. 72 | if s.definition.Source.Suite != "" { 73 | args = append(args, s.definition.Source.Suite, s.rootfsDir) 74 | } else { 75 | args = append(args, s.definition.Image.Release, s.rootfsDir) 76 | } 77 | 78 | if s.definition.Source.URL != "" { 79 | args = append(args, s.definition.Source.URL) 80 | } 81 | 82 | // If s.definition.Source.SameAs is set, create a symlink in /usr/share/debootstrap/scripts 83 | // pointing release to s.definition.Source.SameAs. 84 | scriptPath := filepath.Join("/usr/share/debootstrap/scripts", s.definition.Image.Release) 85 | if !incus.PathExists(scriptPath) && s.definition.Source.SameAs != "" { 86 | err := os.Symlink(s.definition.Source.SameAs, scriptPath) 87 | if err != nil { 88 | return fmt.Errorf("Failed to create symlink: %w", err) 89 | } 90 | 91 | defer os.Remove(scriptPath) 92 | } 93 | 94 | err := shared.RunCommand(s.ctx, nil, nil, "debootstrap", args...) 95 | if err != nil { 96 | return fmt.Errorf(`Failed to run "debootstrap": %w`, err) 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /generators/template_test.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/lxc/distrobuilder/image" 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | func TestTemplateGeneratorRunIncus(t *testing.T) { 16 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 17 | require.NoError(t, err) 18 | 19 | rootfsDir := filepath.Join(cacheDir, "rootfs") 20 | 21 | setup(t, cacheDir) 22 | defer teardown(cacheDir) 23 | 24 | definition := shared.Definition{ 25 | Image: shared.DefinitionImage{ 26 | Distribution: "ubuntu", 27 | Release: "artful", 28 | }, 29 | } 30 | 31 | generator, err := Load("template", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 32 | Generator: "template", 33 | Name: "template", 34 | Content: "==test==", 35 | Path: "/root/template", 36 | }, definition) 37 | require.IsType(t, &template{}, generator) 38 | require.NoError(t, err) 39 | 40 | image := image.NewIncusImage(context.TODO(), cacheDir, "", cacheDir, definition) 41 | 42 | err = os.MkdirAll(filepath.Join(cacheDir, "rootfs", "root"), 0o755) 43 | require.NoError(t, err) 44 | 45 | createTestFile(t, filepath.Join(cacheDir, "rootfs", "root", "template"), "--test--") 46 | 47 | err = generator.RunIncus(image, shared.DefinitionTargetIncus{}) 48 | require.NoError(t, err) 49 | 50 | validateTestFile(t, filepath.Join(cacheDir, "templates", "template.tpl"), "==test==\n") 51 | validateTestFile(t, filepath.Join(cacheDir, "rootfs", "root", "template"), "--test--") 52 | } 53 | 54 | func TestTemplateGeneratorRunIncusDefaultWhen(t *testing.T) { 55 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 56 | require.NoError(t, err) 57 | 58 | rootfsDir := filepath.Join(cacheDir, "rootfs") 59 | 60 | setup(t, cacheDir) 61 | defer teardown(cacheDir) 62 | 63 | definition := shared.Definition{ 64 | Image: shared.DefinitionImage{ 65 | Distribution: "ubuntu", 66 | Release: "artful", 67 | }, 68 | } 69 | 70 | generator, err := Load("template", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 71 | Generator: "template", 72 | Name: "test-default-when", 73 | Content: "==test==", 74 | Path: "test-default-when", 75 | }, definition) 76 | require.IsType(t, &template{}, generator) 77 | require.NoError(t, err) 78 | 79 | image := image.NewIncusImage(context.TODO(), cacheDir, "", cacheDir, definition) 80 | 81 | err = generator.RunIncus(image, shared.DefinitionTargetIncus{}) 82 | require.NoError(t, err) 83 | 84 | generator, err = Load("template", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 85 | Generator: "template", 86 | Name: "test-when", 87 | Content: "==test==", 88 | Path: "test-when", 89 | Template: shared.DefinitionFileTemplate{ 90 | When: []string{"create"}, 91 | }, 92 | }, definition) 93 | require.IsType(t, &template{}, generator) 94 | require.NoError(t, err) 95 | 96 | err = generator.RunIncus(image, shared.DefinitionTargetIncus{}) 97 | require.NoError(t, err) 98 | 99 | testvalue := []string{"create", "copy"} 100 | require.Equal(t, image.Metadata.Templates["test-default-when"].When, testvalue) 101 | 102 | testvalue = []string{"create"} 103 | require.Equal(t, image.Metadata.Templates["test-when"].When, testvalue) 104 | } 105 | -------------------------------------------------------------------------------- /sources/vyos-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/google/go-github/v56/github" 10 | "golang.org/x/sys/unix" 11 | 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | type vyos struct { 16 | common 17 | 18 | fname string 19 | fpath string 20 | } 21 | 22 | func (s *vyos) Run() error { 23 | err := s.downloadImage(s.definition) 24 | if err != nil { 25 | return fmt.Errorf("Failed to download image: %w", err) 26 | } 27 | 28 | return s.unpackISO(filepath.Join(s.fpath, s.fname), s.rootfsDir) 29 | } 30 | 31 | func (s *vyos) downloadImage(definition shared.Definition) error { 32 | var err error 33 | 34 | ctx := context.Background() 35 | client := github.NewClient(nil) 36 | owner := "vyos" 37 | repo := "vyos-rolling-nightly-builds" 38 | 39 | latestRelease, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) 40 | if err != nil { 41 | return fmt.Errorf("Failed to get latest release, %w", err) 42 | } 43 | 44 | isoURL := "" 45 | assets := latestRelease.Assets 46 | for _, a := range assets { 47 | ext := filepath.Ext(a.GetName()) 48 | if ext == ".iso" { 49 | isoURL = a.GetBrowserDownloadURL() 50 | s.fname = a.GetName() 51 | } 52 | } 53 | 54 | if isoURL == "" { 55 | return fmt.Errorf("Failed to get latest release URL.") 56 | } 57 | 58 | s.fpath, err = s.DownloadHash(s.definition.Image, isoURL, "", nil) 59 | 60 | return err 61 | } 62 | 63 | func (s *vyos) unpackISO(filePath string, rootfsDir string) error { 64 | isoDir, err := os.MkdirTemp(s.cacheDir, "temp_") 65 | if err != nil { 66 | return fmt.Errorf("Failed creating temporary directory: %w", err) 67 | } 68 | 69 | defer os.RemoveAll(isoDir) 70 | 71 | squashfsDir, err := os.MkdirTemp(s.cacheDir, "temp_") 72 | if err != nil { 73 | return fmt.Errorf("Failed creating temporary directory: %w", err) 74 | } 75 | 76 | defer os.RemoveAll(squashfsDir) 77 | 78 | // this is easier than doing the whole loop thing ourselves 79 | err = shared.RunCommand(s.ctx, nil, nil, "mount", "-t", "iso9660", "-o", "ro", filePath, isoDir) 80 | if err != nil { 81 | return fmt.Errorf("Failed mounting %q: %w", filePath, err) 82 | } 83 | 84 | defer func() { 85 | _ = unix.Unmount(isoDir, 0) 86 | }() 87 | 88 | squashfsImage := filepath.Join(isoDir, "live", "filesystem.squashfs") 89 | 90 | // The squashfs.img contains an image containing the rootfs, so first 91 | // mount squashfs.img 92 | err = shared.RunCommand(s.ctx, nil, nil, "mount", "-t", "squashfs", "-o", "ro", squashfsImage, squashfsDir) 93 | if err != nil { 94 | return fmt.Errorf("Failed mounting %q: %w", squashfsImage, err) 95 | } 96 | 97 | defer func() { 98 | _ = unix.Unmount(squashfsDir, 0) 99 | }() 100 | 101 | // Remove rootfsDir otherwise rsync will copy the content into the directory 102 | // itself 103 | err = os.RemoveAll(rootfsDir) 104 | if err != nil { 105 | return fmt.Errorf("Failed removing directory %q: %w", rootfsDir, err) 106 | } 107 | 108 | s.logger.WithField("file", squashfsImage).Info("Unpacking root image") 109 | 110 | // Since rootfs is read-only, we need to copy it to a temporary rootfs 111 | // directory in order to create the minimal rootfs. 112 | err = shared.RsyncLocal(s.ctx, squashfsDir+"/", rootfsDir) 113 | if err != nil { 114 | return fmt.Errorf("Failed running rsync: %w", err) 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /.sphinx/wordlist.txt: -------------------------------------------------------------------------------- 1 | AAAA 2 | ABI 3 | ACL 4 | ACLs 5 | AIO 6 | APIs 7 | AppArmor 8 | ArchLinux 9 | ARMv 10 | ARP 11 | ASN 12 | AXFR 13 | backend 14 | backends 15 | balancer 16 | balancers 17 | benchmarking 18 | BGP 19 | bibi 20 | bool 21 | bootable 22 | Btrfs 23 | bugfix 24 | bugfixes 25 | Centos 26 | Ceph 27 | CephFS 28 | Ceph's 29 | CFS 30 | cgroup 31 | cgroupfs 32 | cgroups 33 | checksum 34 | checksums 35 | Chocolatey 36 | CIDR 37 | CLI 38 | CPUs 39 | CRIU 40 | CRL 41 | cron 42 | CSV 43 | CUDA 44 | customizable 45 | dataset 46 | DCO 47 | dereferenced 48 | devtmpfs 49 | DHCP 50 | DHCPv 51 | distrobuilder 52 | DNS 53 | DNSSEC 54 | DoS 55 | downloader 56 | downloaders 57 | Dqlite 58 | DRM 59 | EB 60 | Ebit 61 | eBPF 62 | ECDHE 63 | ECDSA 64 | EiB 65 | Eibit 66 | endian 67 | ESA 68 | ETag 69 | failover 70 | FQDNs 71 | gapped 72 | GARP 73 | GbE 74 | Gbit 75 | Geneve 76 | GiB 77 | Gibit 78 | GID 79 | GIDs 80 | Golang 81 | goroutines 82 | GPG 83 | GPUs 84 | Grafana 85 | HAProxy 86 | hardcoded 87 | Homebrew 88 | hotplug 89 | hotplugged 90 | hotplugging 91 | HTTPS 92 | ICMP 93 | idmap 94 | idmapped 95 | idmaps 96 | incrementing 97 | Incus 98 | InfiniBand 99 | InfluxDB 100 | init 101 | initramfs 102 | integrations 103 | IOPS 104 | IOV 105 | IPs 106 | IPv 107 | IPVLAN 108 | jq 109 | JSON 110 | kB 111 | kbit 112 | KiB 113 | kibi 114 | Kibit 115 | lookups 116 | LTS 117 | LV 118 | LVM 119 | LXC 120 | LXCFS 121 | MAAS 122 | macOS 123 | macvlan 124 | Makefile 125 | manpages 126 | Mbit 127 | MiB 128 | Mibit 129 | MicroCeph 130 | MicroCloud 131 | MII 132 | MITM 133 | MTU 134 | multicast 135 | MyST 136 | namespace 137 | namespaced 138 | namespaces 139 | NATed 140 | natively 141 | NDP 142 | netmask 143 | NFS 144 | NIC 145 | NICs 146 | NUMA 147 | NVRAM 148 | OData 149 | OpenMetrics 150 | OpenSSL 151 | OSD 152 | overcommit 153 | overcommitting 154 | overlayfs 155 | OVMF 156 | OVN 157 | OVS 158 | Pbit 159 | PCI 160 | PCIe 161 | peerings 162 | Permalink 163 | PFs 164 | PiB 165 | Pibit 166 | PID 167 | PKI 168 | PNG 169 | Pongo 170 | POSIX 171 | pre 172 | preseed 173 | proxied 174 | proxying 175 | PTS 176 | QEMU 177 | qgroup 178 | qgroups 179 | RADOS 180 | RBAC 181 | RBD 182 | reconfiguring 183 | requestor 184 | RESTful 185 | RHEL 186 | rootfs 187 | RSA 188 | rST 189 | runtime 190 | SATA 191 | scalable 192 | SDN 193 | Seccomp 194 | SFTP 195 | SHA 196 | shiftfs 197 | SIGHUP 198 | SIGTERM 199 | simplestreams 200 | SLAAC 201 | SMTP 202 | Solaris 203 | SPAs 204 | SPL 205 | SquashFS 206 | SSDs 207 | SSL 208 | stateful 209 | stderr 210 | stdin 211 | stdout 212 | STP 213 | struct 214 | structs 215 | subcommands 216 | subitem 217 | subnet 218 | subnets 219 | subpage 220 | substep 221 | subtree 222 | subtrees 223 | subvolume 224 | subvolumes 225 | superset 226 | SVG 227 | symlink 228 | symlinks 229 | syscall 230 | syscalls 231 | sysfs 232 | Tbit 233 | TCP 234 | Telegraf 235 | TiB 236 | Tibit 237 | TLS 238 | tmpfs 239 | toolchain 240 | topologies 241 | TPM 242 | TSIG 243 | TTL 244 | TTYs 245 | UDP 246 | UEFI 247 | UFW 248 | UI 249 | UID 250 | UIDs 251 | unconfigured 252 | unmanaged 253 | unmount 254 | unmounting 255 | uplink 256 | uptime 257 | userspace 258 | UUID 259 | vCPU 260 | vCPUs 261 | VFs 262 | VFS 263 | VirtIO 264 | virtualize 265 | virtualized 266 | VLAN 267 | VLANs 268 | VM 269 | VMs 270 | VPD 271 | VPS 272 | vSwitch 273 | VXLAN 274 | WebSocket 275 | WebSockets 276 | XFS 277 | XHR 278 | XP 279 | YAML 280 | Zettabyte 281 | ZFS 282 | zpool 283 | zpools 284 | -------------------------------------------------------------------------------- /sources/testdata/key4.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFmFVJkBCACzAXGvRdC4rBfgOHGPmk7+apjnhhO8GYX/igc2Qk4jn68TewJA 4 | Dxw3VFfn9yBzbV27WuFypN98D2Gw6DE+FXmxXqCx8ERj41A97VYWT0gQedWlIS+Y 5 | QzawbLcpK0Ei+LABL33eDff+IaeHS8GgFqvfEaQMU2pf9oMFxQtAM5zfgHRUD5bM 6 | 5TEHnY4sRVa8Bm9VbzJKbyiQkI7Kzz5bVM/mbgQrwZL14BixytzR1XfuGBmDOC5D 7 | dKVG8aHzKIQqV5mKQgfrclsVIlZhl+ZuTK0aCM6RDbsUI+2ORW1rW9f4EOk77guY 8 | 9m1Gn2+M/FqqPtK/OlgoDYV8/nWS9N+kR8n5ABEBAAG0MlZvaWQgTGludXggSW1h 9 | Z2UgU2lnbmluZyBLZXkgPGltYWdlc0B2b2lkbGludXguZXU+iQFUBBMBCAA+FiEE 10 | zyS5wDgJfYpElY4sjevaaLSCgqQFAlmFVJkCGwMFCQPCZwAFCwkIBwIGFQgJCgsC 11 | BBYCAwECHgECF4AACgkQjevaaLSCgqSrbwf8CMjE1iBgFk2U+LdrUhoRf4i05EWE 12 | SrPIvY7516/S8SE5wlqZ8SEj4kHvE7FgUbWsZInigKehW6YLgN4eGioLUCqn72C9 13 | jH3mp194ENhYAVd9b7CA3dXoYlLmfmb5Sp9dExbadada3pFlBdalSXS4Vfmsnq/m 14 | q9Pp0gFrStRYzJsGiOS+XmadQpJSbc+DUI4k27cb/MIWLCR9PmjsVtJtyeC/Wf1h 15 | AlL5yfSbYDHEq/jONIVGm2kjGpNaThKeYHhMiQ6cCA26lyCDY35LswFWQAUEYRRN 16 | MlpGfHJ5fASiDZ0Rkay8DbhzR7Ee7azn6UQhx3cmGSzFZ2BjzWlDRBVB4okCMwQQ 17 | AQgAHRYhBIXw4r0w8iqe6UpAkn2VOHlWDxfCBQJc1DuJAAoJEH2VOHlWDxfCk+wQ 18 | AJz4kjqcuFA/SUYdHNrnohp65+FZPex392tt+6yjwfFnItN6SV5STItNNQpR1deS 19 | K/XokukSxNhW27vVd6BcmdYVUlKOs+G66f2es2lQhbm5lnfe0NRFxuuejKAvawnc 20 | VIQKrrS7LStA2zeGDpSG0rH9Q9Yk2h1K8tViWgJ4thqL4Gd5Q4XGfQK0pDR8czu9 21 | DitTMJL//E4vAdyEhdjawdCJ1i5A2n+HIijXMShc6yNuBL4je/PrALaiu53Ne36P 22 | d55wqTBUzEwa5hkD/BmPvK6T09lddND0fGp2ZRNiSyQBXAzo5vLPJQ3kdpUH405T 23 | 7cKxWpGQtyM7N+clOdlT4BqLcROmK9a6hRb58SyE+iuJ1iJ9Lny1Sxy1jdRXJYFN 24 | q18IkbnOfcxW5t9L/ag49loTWP6K9oItxee9gqfuYJCIPwyoXdn/I1w+HG/XOH0L 25 | jh/FdsD4nPj9AzX5lbbS5FqW3nKu0OIGRDqFUeBwpFFDK8d/6paJLKLqtIi6Do83 26 | 3Oo/bQDyqntcym2+veKA085tzAif4/9ZtKvn0vIZbUa6pQz6I3QSL/w5hNa2bgjV 27 | 5Am/sQN8Btsq6TJlgJwRQbF6UHt1tcQswfRMbkzmftuhsItKJcNTai7ZbSvmeAkn 28 | lBGujj3kE2ymzN8znB3q2uXDIYa1WvM8zr6H/v0SB6NSiQIzBBABCAAdFiEE1JVO 29 | DBmUbHx0bgbNzJsdz6wQpZ8FAltqFs0ACgkQzJsdz6wQpZ+jpxAA1ojOGSkuDF89 30 | 0iiuB6VoJxJC14wxzLoRpQ1Mc5xxVQfgZuzyeK+sLX6wHYIFwAsjysfAYa5GknGH 31 | 2indYbhfbHPM08WD/TvIFHfCXmw8Y5lc/trh4dD1hDWaWU/PgwOQFox/sYkE58oD 32 | JTqrL2EQasJGxpZUBY1c6aXbPXBCTmu72tgEfqOtOA8/keHr4WrfiFkHr21JspIu 33 | jSR63W3CMASolag+cmIi51Ei259V4dER1dGg+2dBnWRC66aXZbUM4GAwCKPRm3la 34 | bGdl9UhcRK7+RVOVnO1FgzVck0pCLHayp84nbPxSR6mgcl0YlIOPi6B67+oi6nH7 35 | hAnq1h2/A863sC1QLbBxgLCskMKkjYOyJyEAxKPJPGDGw+EFldKagvHLX3a6/ZdL 36 | MF9ugdPNZhWRBqjPvWm2h3C7Fcnp48eL9KPG3LG96I7IGMa/tdMeTXFWnNq3s1ht 37 | CCr77wwNejm/lCyJRhByKqEekRso2JhVPxh4FUZipqM74gq4gb4h+sL/8x2dQnrx 38 | oh4r3A11DvyDYOd0miuPQvcze0eYxXhwTKoj7IKPHSbq+dMBTacaFGKBliiQAnq6 39 | QAkG5GQ3xWBe7Qvc+XrINtvnA22MhuPs5PxMzycbiVI0sBZabfJRenxEgQ0hKcAS 40 | 4A2WoT2QYU5M/RN0iTIBpJ5fGrw4yKO5AQ0EWYVUmQEIAKKDLexgCNcrlAR4sQil 41 | 68QOrMqP/SKk7jUm1/9E9Pz4mRJ+EHwRRpAnYD5i1qKQN9C7iFxgaQztYs1tPrtp 42 | Z/nkVbbmDGSayBUQ9MH3sOc2SkLzsiuzE4vgRYFXHAsqzKazseQJiHe3sHee6ROI 43 | ncChhmb3PQzPFLo6/iGej88CwSc/I6DK+VtfWmyodf5znEnAv4fX/FPj/UM7XnAw 44 | tGL9LCP7Wk1AZ+hpEBhzLnfU5K9TIoFzJAEfG8A/lH9WN9e7uWgCjuc5d5kpZRna 45 | W6faGLgj1tHfn91xUHg0FZAM+ehkcwNmexo7vBs2I9Rj0BHH7yWWgSj9ucgzegJU 46 | 070AEQEAAYkBPAQYAQgAJhYhBM8kucA4CX2KRJWOLI3r2mi0goKkBQJZhVSZAhsM 47 | BQkDwmcAAAoJEI3r2mi0goKknmkIAI3VkJ7zeEQ6NQ7NvkcqiD7JWp2UjHt51n0E 48 | MYg8dLU6Z24F7fLbtH6O090iz5ZPN/u2cFX1NfcBXKb47/QWxDCfPhw5+/hNUqUn 49 | q80sHWeqBEznamxp0nx5h1x2Hb5lUvTGWj327H4iprZC1xesb+P5TWd9pt0egjgg 50 | pq9CD5sjsaMFdSzsgUFAMtDpeJ4sqJiaJE8/vJBoaPhk/jfs/OOEdFnQ0jiv8xLt 51 | aJJU9TBAtb6B8Vd4ZcFjlid/HroUvDGT2x7fundHLwMVduhNDSDNgzwOSHpckkNb 52 | db4nlXafR9hozvGboYm2BehVOCk+3gtgURYpfu/0G1Jn0OpOPHM= 53 | =DFTs 54 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /doc/reference/packages.md: -------------------------------------------------------------------------------- 1 | # Package management 2 | 3 | Installing and removing packages can be done using the `packages` section. 4 | 5 | ```yaml 6 | packages: 7 | manager: # required 8 | update: 9 | cleanup: 10 | sets: 11 | - packages: 12 | - 13 | - ... 14 | action: # required 15 | architectures: # filter 16 | releases: # filter 17 | variants: # filter 18 | flags: # install/remove flags for just this set 19 | - ... 20 | repositories: 21 | - name: 22 | url: 23 | type: 24 | key: 25 | architectures: # filter 26 | releases: # filter 27 | variants: # filter 28 | - ... 29 | 30 | ``` 31 | 32 | The `manager` keys specifies the package manager which is to be used. 33 | Valid package manager are: 34 | 35 | * `apk` 36 | * `apt` 37 | * `dnf` 38 | * `egoportage` (combination of `portage` and `ego`) 39 | * `equo` 40 | * `anise` 41 | * `opkg` 42 | * `pacman` 43 | * `portage` 44 | * `slackpkg` 45 | * `xbps` 46 | * `yum` 47 | * `zypper` 48 | 49 | It's also possible to specify a custom package manager. 50 | This is useful if the desired package manager is not supported by distrobuilder. 51 | 52 | ```yaml 53 | packages: 54 | custom_manager: # required 55 | clean: # required 56 | cmd: 57 | flags: 58 | install: # required 59 | cmd: 60 | flags: 61 | remove: # required 62 | cmd: 63 | flags: 64 | refresh: # required 65 | cmd: 66 | flags: 67 | update: # required 68 | cmd: 69 | flags: 70 | flags: # global flags for all commands 71 | ... 72 | ``` 73 | 74 | If `update` is true, the package manager will update all installed packages. 75 | 76 | If `cleanup` is true, the package manager will run a cleanup operation which usually cleans up cached files. 77 | This depends on the package manager though and is not supported by all. 78 | 79 | A set contains a list of `packages`, an `action`, and optional filters. 80 | Here, `packages` is a list of packages which are to be installed or removed. 81 | The value of `action` must be either `install` or `remove`. If `flags` is 82 | specified for a package set, they are appended to the command specific 83 | flags, along with any global flags, when calling the `install` or `remove` 84 | command. For example, you can define a package set that should be installed 85 | with `--no-install-recommends`. 86 | 87 | `repositories` contains a list of additional repositories which are to be added. 88 | The `type` field is only needed if the package manager supports more than one repository manager. 89 | The `key` field is a GPG armored key ring which might be needed for verification. 90 | 91 | Depending on the package manager, the `url` field can take the content of a repository file. The following is possible with `yum`: 92 | 93 | ```yaml 94 | packages: 95 | manager: yum 96 | update: false 97 | repositories: 98 | - name: myrepo 99 | url: |- 100 | [myrepo] 101 | baseurl=http://user:password@1.1.1.1 102 | gpgcheck=0 103 | ``` 104 | -------------------------------------------------------------------------------- /managers/pacman.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "slices" 9 | 10 | "github.com/lxc/distrobuilder/shared" 11 | ) 12 | 13 | type pacman struct { 14 | common 15 | } 16 | 17 | func (m *pacman) load() error { 18 | err := m.setMirrorlist() 19 | if err != nil { 20 | return fmt.Errorf("Failed to set mirrorlist: %w", err) 21 | } 22 | 23 | err = m.setupTrustedKeys() 24 | if err != nil { 25 | return fmt.Errorf("Failed to setup trusted keys: %w", err) 26 | } 27 | 28 | m.commands = managerCommands{ 29 | clean: "pacman", 30 | install: "pacman", 31 | refresh: "pacman", 32 | remove: "pacman", 33 | update: "pacman", 34 | } 35 | 36 | m.flags = managerFlags{ 37 | clean: []string{ 38 | "-Sc", 39 | }, 40 | global: []string{ 41 | "--noconfirm", 42 | }, 43 | install: []string{ 44 | "-S", "--needed", 45 | }, 46 | remove: []string{ 47 | "-Rcs", 48 | }, 49 | refresh: []string{ 50 | "-Syy", 51 | }, 52 | update: []string{ 53 | "-Su", 54 | }, 55 | } 56 | 57 | m.hooks = managerHooks{ 58 | clean: func() error { 59 | path := "/var/cache/pacman/pkg" 60 | 61 | // List all entries. 62 | entries, err := os.ReadDir(path) 63 | if err != nil { 64 | if os.IsNotExist(err) { 65 | return nil 66 | } 67 | 68 | return fmt.Errorf("Failed to list directory '%s': %w", path, err) 69 | } 70 | 71 | // Individually wipe all entries. 72 | for _, entry := range entries { 73 | entryPath := filepath.Join(path, entry.Name()) 74 | err := os.RemoveAll(entryPath) 75 | if err != nil && !os.IsNotExist(err) { 76 | return fmt.Errorf("Failed to remove '%s': %w", entryPath, err) 77 | } 78 | } 79 | 80 | return nil 81 | }, 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func (m *pacman) setupTrustedKeys() error { 88 | var err error 89 | 90 | _, err = os.Stat("/etc/pacman.d/gnupg") 91 | if err == nil { 92 | return nil 93 | } 94 | 95 | err = shared.RunCommand(m.ctx, nil, nil, "pacman-key", "--init") 96 | if err != nil { 97 | return fmt.Errorf("Error initializing with pacman-key: %w", err) 98 | } 99 | 100 | var keyring string 101 | 102 | if slices.Contains([]string{"arm", "arm64"}, runtime.GOARCH) { 103 | keyring = "archlinuxarm" 104 | } else { 105 | keyring = "archlinux" 106 | } 107 | 108 | err = shared.RunCommand(m.ctx, nil, nil, "pacman-key", "--populate", keyring) 109 | if err != nil { 110 | return fmt.Errorf("Error populating with pacman-key: %w", err) 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (m *pacman) setMirrorlist() error { 117 | f, err := os.Create(filepath.Join("etc", "pacman.d", "mirrorlist")) 118 | if err != nil { 119 | return fmt.Errorf("Failed to create file %q: %w", filepath.Join("etc", "pacman.d", "mirrorlist"), err) 120 | } 121 | 122 | defer f.Close() 123 | 124 | var mirror string 125 | 126 | if slices.Contains([]string{"arm", "arm64"}, runtime.GOARCH) { 127 | mirror = "Server = http://mirror.archlinuxarm.org/$arch/$repo" 128 | } else if slices.Contains([]string{"riscv64"}, runtime.GOARCH) { 129 | mirror = "Server = https://archriscv.felixc.at/repo/$repo" 130 | } else { 131 | mirror = "Server = http://mirrors.kernel.org/archlinux/$repo/os/$arch" 132 | } 133 | 134 | _, err = f.WriteString(mirror) 135 | if err != nil { 136 | return fmt.Errorf("Failed to write to %q: %w", filepath.Join("etc", "pacman.d", "mirrorlist"), err) 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /sources/apertis-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/lxc/distrobuilder/shared" 14 | ) 15 | 16 | type apertis struct { 17 | common 18 | } 19 | 20 | // Run downloads the tarball and unpacks it. 21 | func (s *apertis) Run() error { 22 | release := s.definition.Image.Release 23 | exactRelease := release 24 | 25 | // https://images.apertis.org/daily/v2020dev0/20190830.0/amd64/minimal/ospack_v2020dev0-amd64-minimal_20190830.0.tar.gz 26 | baseURL := fmt.Sprintf("%s/%s/%s", 27 | s.definition.Source.URL, s.definition.Source.Variant, release) 28 | 29 | var ( 30 | resp *http.Response 31 | err error 32 | ) 33 | 34 | err = shared.Retry(func() error { 35 | resp, err = s.client.Head(baseURL) 36 | if err != nil { 37 | return fmt.Errorf("Failed to HEAD %q: %w", baseURL, err) 38 | } 39 | 40 | return nil 41 | }, 3) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if resp.StatusCode == http.StatusNotFound { 47 | // Possibly, release is a specific release (18.12.0 instead of 18.12). Lets trim the prefix and continue. 48 | re := regexp.MustCompile(`\.\d+$`) 49 | release = strings.TrimSuffix(release, re.FindString(release)) 50 | baseURL = fmt.Sprintf("%s/%s/%s", 51 | s.definition.Source.URL, s.definition.Source.Variant, release) 52 | } else { 53 | exactRelease, err = s.getLatestRelease(baseURL, release) 54 | if err != nil { 55 | return fmt.Errorf("Failed to get latest release: %w", err) 56 | } 57 | } 58 | 59 | baseURL = fmt.Sprintf("%s/%s/%s/%s/", 60 | baseURL, exactRelease, s.definition.Image.ArchitectureMapped, s.definition.Image.Variant) 61 | fname := fmt.Sprintf("ospack_%s-%s-%s_%s.tar.gz", 62 | release, s.definition.Image.ArchitectureMapped, s.definition.Image.Variant, exactRelease) 63 | 64 | url, err := url.Parse(baseURL) 65 | if err != nil { 66 | return fmt.Errorf("Failed to parse %q: %w", baseURL, err) 67 | } 68 | 69 | // Force gpg checks when using http 70 | if url.Scheme != "https" { 71 | return errors.New("Only HTTPS server is supported") 72 | } 73 | 74 | fpath, err := s.DownloadHash(s.definition.Image, baseURL+fname, "", nil) 75 | if err != nil { 76 | return fmt.Errorf("Failed to download %q: %w", baseURL+fname, err) 77 | } 78 | 79 | s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image") 80 | 81 | // Unpack 82 | err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir) 83 | if err != nil { 84 | return fmt.Errorf("Failed to unpack %q: %w", fname, err) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (s *apertis) getLatestRelease(baseURL, release string) (string, error) { 91 | var ( 92 | resp *http.Response 93 | err error 94 | ) 95 | 96 | err = shared.Retry(func() error { 97 | resp, err = s.client.Get(baseURL) 98 | if err != nil { 99 | return fmt.Errorf("Failed to GET %q: %w", baseURL, err) 100 | } 101 | 102 | return nil 103 | }, 3) 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | defer resp.Body.Close() 109 | 110 | body, err := io.ReadAll(resp.Body) 111 | if err != nil { 112 | return "", fmt.Errorf("Failed to ready body: %w", err) 113 | } 114 | 115 | regex := regexp.MustCompile(fmt.Sprintf(">(%s\\.\\d+)/<", release)) 116 | releases := regex.FindAllStringSubmatch(string(body), -1) 117 | 118 | if len(releases) > 0 { 119 | return releases[len(releases)-1][1], nil 120 | } 121 | 122 | return "", nil 123 | } 124 | -------------------------------------------------------------------------------- /sources/common_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "testing" 10 | 11 | incus "github.com/lxc/incus/v6/shared/util" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/lxc/distrobuilder/shared" 15 | ) 16 | 17 | func TestVerifyFile(t *testing.T) { 18 | wd, err := os.Getwd() 19 | if err != nil { 20 | t.Fatalf("Failed to retrieve working directory: %v", err) 21 | } 22 | 23 | testdataDir := filepath.Join(wd, "..", "testdata") 24 | 25 | keys := []string{"0x5DE8949A899C8D99"} 26 | keyserver := "keyserver.ubuntu.com" 27 | 28 | tests := []struct { 29 | name string 30 | signedFile string 31 | signatureFile string 32 | keys []string 33 | keyserver string 34 | shouldFail bool 35 | }{ 36 | { 37 | "testfile with detached signature", 38 | filepath.Join(testdataDir, "testfile"), 39 | filepath.Join(testdataDir, "testfile.sig"), 40 | keys, 41 | keyserver, 42 | false, 43 | }, 44 | { 45 | "testfile with cleartext signature", 46 | filepath.Join(testdataDir, "testfile.asc"), 47 | "", 48 | keys, 49 | keyserver, 50 | false, 51 | }, 52 | { 53 | "testfile with invalid cleartext signature", 54 | filepath.Join(testdataDir, "testfile-invalid.asc"), 55 | "", 56 | keys, 57 | keyserver, 58 | true, 59 | }, 60 | { 61 | "testfile with normal signature", 62 | filepath.Join(testdataDir, "testfile.gpg"), 63 | "", 64 | keys, 65 | keyserver, 66 | false, 67 | }, 68 | { 69 | "no keys", 70 | filepath.Join(testdataDir, "testfile"), 71 | filepath.Join(testdataDir, "testfile.sig"), 72 | []string{}, 73 | keyserver, 74 | true, 75 | }, 76 | { 77 | "invalid key", 78 | filepath.Join(testdataDir, "testfile.asc"), 79 | "", 80 | []string{"0x46181433FBB75451"}, 81 | keyserver, 82 | true, 83 | }, 84 | } 85 | 86 | c := common{ 87 | sourcesDir: os.TempDir(), 88 | definition: shared.Definition{ 89 | Source: shared.DefinitionSource{}, 90 | }, 91 | ctx: context.TODO(), 92 | } 93 | 94 | for i, tt := range tests { 95 | log.Printf("Running test #%d: %s", i, tt.name) 96 | 97 | c.definition = shared.Definition{ 98 | Source: shared.DefinitionSource{ 99 | Keyserver: tt.keyserver, 100 | Keys: tt.keys, 101 | }, 102 | } 103 | 104 | valid, err := c.VerifyFile(tt.signedFile, tt.signatureFile) 105 | if tt.shouldFail { 106 | require.Error(t, err) 107 | require.False(t, valid) 108 | } else { 109 | require.NoError(t, err) 110 | require.True(t, valid) 111 | } 112 | } 113 | } 114 | 115 | func TestCreateGPGKeyring(t *testing.T) { 116 | c := common{ 117 | sourcesDir: os.TempDir(), 118 | definition: shared.Definition{ 119 | Source: shared.DefinitionSource{ 120 | Keyserver: "keyserver.ubuntu.com", 121 | Keys: []string{"0x5DE8949A899C8D99"}, 122 | }, 123 | }, 124 | ctx: context.TODO(), 125 | } 126 | 127 | keyring, err := c.CreateGPGKeyring() 128 | require.NoError(t, err) 129 | 130 | require.FileExists(t, keyring) 131 | os.RemoveAll(path.Dir(keyring)) 132 | 133 | c.definition = shared.Definition{} 134 | 135 | // This shouldn't fail, but the keyring file should not be created since 136 | // there are no keys to be exported. 137 | keyring, err = c.CreateGPGKeyring() 138 | require.NoError(t, err) 139 | 140 | require.False(t, incus.PathExists(keyring), "File should not exist") 141 | os.RemoveAll(path.Dir(keyring)) 142 | } 143 | -------------------------------------------------------------------------------- /sources/docker.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | imgspec "github.com/opencontainers/image-spec/specs-go/v1" 11 | "github.com/opencontainers/umoci/oci/cas/dir" 12 | "github.com/opencontainers/umoci/oci/casext" 13 | "github.com/opencontainers/umoci/oci/layer" 14 | "go.podman.io/image/v5/copy" 15 | "go.podman.io/image/v5/docker/reference" 16 | "go.podman.io/image/v5/oci/layout" 17 | "go.podman.io/image/v5/signature" 18 | "go.podman.io/image/v5/transports/alltransports" 19 | "go.podman.io/image/v5/types" 20 | ) 21 | 22 | type docker struct { 23 | common 24 | } 25 | 26 | // Run downloads and unpacks a docker image. 27 | func (s *docker) Run() error { 28 | absRootfsDir, err := filepath.Abs(s.rootfsDir) 29 | if err != nil { 30 | return fmt.Errorf("Failed to get absolute path of %s: %w", s.rootfsDir, err) 31 | } 32 | 33 | // Get some temporary storage. 34 | ociPath, err := os.MkdirTemp("", "incus-oci-") 35 | if err != nil { 36 | return err 37 | } 38 | 39 | defer func() { _ = os.RemoveAll(ociPath) }() 40 | 41 | // Parse the image reference 42 | imageRef, err := reference.ParseNormalizedNamed(s.definition.Source.URL) 43 | if err != nil { 44 | return fmt.Errorf("Failed to parse image reference: %w", err) 45 | } 46 | 47 | // Docker references with both a tag and digest are currently not supported 48 | var imageTag string 49 | digested, ok := imageRef.(reference.Digested) 50 | if ok { 51 | imageTag = digested.Digest().String() 52 | } else { 53 | imageTag = "latest" 54 | tagged, ok := imageRef.(reference.NamedTagged) 55 | if ok { 56 | imageTag = tagged.Tag() 57 | } 58 | } 59 | 60 | srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", s.definition.Source.URL)) 61 | if err != nil { 62 | return fmt.Errorf("Failed to parse image name: %w", err) 63 | } 64 | 65 | dstRef, err := layout.ParseReference(fmt.Sprintf("%s:%s", ociPath, imageTag)) 66 | if err != nil { 67 | return fmt.Errorf("Failed to parse destination reference: %w", err) 68 | } 69 | 70 | // Create policy context 71 | systemCtx := &types.SystemContext{ 72 | DockerInsecureSkipTLSVerify: types.OptionalBoolFalse, 73 | } 74 | 75 | policy := &signature.Policy{ 76 | Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}, 77 | } 78 | 79 | policyCtx, err := signature.NewPolicyContext(policy) 80 | if err != nil { 81 | return fmt.Errorf("Failed to create policy context: %w", err) 82 | } 83 | 84 | defer func() { _ = policyCtx.Destroy() }() 85 | 86 | copyOptions := ©.Options{ 87 | RemoveSignatures: true, 88 | SourceCtx: systemCtx, 89 | DestinationCtx: systemCtx, 90 | } 91 | 92 | ctx := context.TODO() 93 | 94 | // Pull image from OCI registry 95 | copiedManifest, err := copy.Image(ctx, policyCtx, dstRef, srcRef, copyOptions) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | // Unpack OCI image 101 | unpackOptions := &layer.UnpackOptions{KeepDirlinks: true} 102 | 103 | engine, err := dir.Open(ociPath) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | engineExt := casext.NewEngine(engine) 109 | 110 | defer func() { _ = engine.Close() }() 111 | 112 | var manifest imgspec.Manifest 113 | err = json.Unmarshal(copiedManifest, &manifest) 114 | if err != nil { 115 | return fmt.Errorf("Failed to parse manifest: %w", err) 116 | } 117 | 118 | return layer.UnpackRootfs(ctx, engineExt, absRootfsDir, manifest, unpackOptions) 119 | } 120 | -------------------------------------------------------------------------------- /generators/hosts.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/lxc/incus/v6/shared/api" 10 | incus "github.com/lxc/incus/v6/shared/util" 11 | 12 | "github.com/lxc/distrobuilder/image" 13 | "github.com/lxc/distrobuilder/shared" 14 | ) 15 | 16 | type hosts struct { 17 | common 18 | } 19 | 20 | // RunLXC creates a LXC specific entry in the hosts file. 21 | func (g *hosts) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error { 22 | // Skip if the file doesn't exist 23 | if !incus.PathExists(filepath.Join(g.sourceDir, g.defFile.Path)) { 24 | return nil 25 | } 26 | 27 | // Read the current content 28 | content, err := os.ReadFile(filepath.Join(g.sourceDir, g.defFile.Path)) 29 | if err != nil { 30 | return fmt.Errorf("Failed to read file %q: %w", filepath.Join(g.sourceDir, g.defFile.Path), err) 31 | } 32 | 33 | // Replace hostname with placeholder 34 | content = []byte(strings.ReplaceAll(string(content), "distrobuilder", "LXC_NAME")) 35 | 36 | // Add a new line if needed 37 | if !strings.Contains(string(content), "LXC_NAME") { 38 | content = append([]byte("127.0.1.1\tLXC_NAME\n"), content...) 39 | } 40 | 41 | f, err := os.Create(filepath.Join(g.sourceDir, g.defFile.Path)) 42 | if err != nil { 43 | return fmt.Errorf("Failed to create file %q: %w", filepath.Join(g.sourceDir, g.defFile.Path), err) 44 | } 45 | 46 | defer f.Close() 47 | 48 | // Overwrite the file 49 | _, err = f.Write(content) 50 | if err != nil { 51 | return fmt.Errorf("Failed to write to file %q: %w", filepath.Join(g.sourceDir, g.defFile.Path), err) 52 | } 53 | 54 | // Add hostname path to LXC's templates file 55 | err = img.AddTemplate(g.defFile.Path) 56 | if err != nil { 57 | return fmt.Errorf("Failed to add template: %w", err) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // RunIncus creates a hosts template. 64 | func (g *hosts) RunIncus(img *image.IncusImage, target shared.DefinitionTargetIncus) error { 65 | // Skip if the file doesn't exist 66 | if !incus.PathExists(filepath.Join(g.sourceDir, g.defFile.Path)) { 67 | return nil 68 | } 69 | 70 | templateDir := filepath.Join(g.cacheDir, "templates") 71 | 72 | // Create templates path 73 | err := os.MkdirAll(templateDir, 0o755) 74 | if err != nil { 75 | return fmt.Errorf("Failed to create directory %q: %w", templateDir, err) 76 | } 77 | 78 | // Read the current content 79 | content, err := os.ReadFile(filepath.Join(g.sourceDir, g.defFile.Path)) 80 | if err != nil { 81 | return fmt.Errorf("Failed to read file %q: %w", filepath.Join(g.sourceDir, g.defFile.Path), err) 82 | } 83 | 84 | // Replace hostname with placeholder 85 | content = []byte(strings.ReplaceAll(string(content), "distrobuilder", "{{ container.name }}")) 86 | 87 | // Add a new line if needed 88 | if !strings.Contains(string(content), "{{ container.name }}") { 89 | content = append([]byte("127.0.1.1\t{{ container.name }}\n"), content...) 90 | } 91 | 92 | // Write the template 93 | err = os.WriteFile(filepath.Join(templateDir, "hosts.tpl"), content, 0o644) 94 | if err != nil { 95 | return fmt.Errorf("Failed to write file %q: %w", filepath.Join(templateDir, "hosts.tpl"), err) 96 | } 97 | 98 | img.Metadata.Templates[g.defFile.Path] = &api.ImageMetadataTemplate{ 99 | Template: "hosts.tpl", 100 | Properties: g.defFile.Template.Properties, 101 | When: g.defFile.Template.When, 102 | } 103 | 104 | if len(g.defFile.Template.When) == 0 { 105 | img.Metadata.Templates[g.defFile.Path].When = []string{ 106 | "create", 107 | "copy", 108 | } 109 | } 110 | 111 | return nil 112 | } 113 | 114 | // Run does nothing. 115 | func (g *hosts) Run() error { 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /sources/voidlinux-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "crypto/sha256" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/lxc/distrobuilder/shared" 15 | ) 16 | 17 | type voidlinux struct { 18 | common 19 | } 20 | 21 | // Run downloads a Void Linux rootfs tarball. 22 | func (s *voidlinux) Run() error { 23 | baseURL := s.definition.Source.URL 24 | fname, err := s.getLatestBuild(baseURL, s.definition.Image.ArchitectureMapped, s.definition.Source.Variant) 25 | if err != nil { 26 | return fmt.Errorf("Failed to get latest build: %w", err) 27 | } 28 | 29 | if fname == "" { 30 | return errors.New("Failed to determine latest build") 31 | } 32 | 33 | tarball := fmt.Sprintf("%s/%s", baseURL, fname) 34 | digests := fmt.Sprintf("%s/sha256sum.txt", baseURL) 35 | signatures := fmt.Sprintf("%s/sha256sum.sig", baseURL) 36 | 37 | url, err := url.Parse(tarball) 38 | if err != nil { 39 | return fmt.Errorf("Failed to parse URL %q: %w", tarball, err) 40 | } 41 | 42 | if !s.definition.Source.SkipVerification && url.Scheme != "https" && 43 | len(s.definition.Source.Keys) == 0 { 44 | return errors.New("GPG keys are required if downloading from HTTP") 45 | } 46 | 47 | var fpath string 48 | 49 | if s.definition.Source.SkipVerification { 50 | fpath, err = s.DownloadHash(s.definition.Image, tarball, "", nil) 51 | } else { 52 | fpath, err = s.DownloadHash(s.definition.Image, tarball, digests, sha256.New()) 53 | } 54 | 55 | if err != nil { 56 | return fmt.Errorf("Failed to download %q: %w", tarball, err) 57 | } 58 | 59 | // Force gpg checks when using http 60 | if !s.definition.Source.SkipVerification && url.Scheme != "https" { 61 | _, err = s.DownloadHash(s.definition.Image, digests, "", nil) 62 | if err != nil { 63 | return fmt.Errorf("Failed to download %q: %w", digests, err) 64 | } 65 | 66 | _, err = s.DownloadHash(s.definition.Image, signatures, "", nil) 67 | if err != nil { 68 | return fmt.Errorf("Failed to download %q: %w", signatures, err) 69 | } 70 | 71 | valid, err := s.VerifyFile( 72 | filepath.Join(fpath, "sha256sum.txt"), 73 | filepath.Join(fpath, "sha256sum.sig")) 74 | if err != nil { 75 | return fmt.Errorf(`Failed to verify "sha256sum.txt": %w`, err) 76 | } 77 | 78 | if !valid { 79 | return errors.New(`Invalid signature for "sha256sum.txt"`) 80 | } 81 | } 82 | 83 | s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image") 84 | 85 | // Unpack 86 | err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir) 87 | if err != nil { 88 | return fmt.Errorf("Failed to unpack %q: %w", filepath.Join(fpath, fname), err) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (s *voidlinux) getLatestBuild(baseURL, arch, variant string) (string, error) { 95 | var ( 96 | resp *http.Response 97 | err error 98 | ) 99 | 100 | err = shared.Retry(func() error { 101 | resp, err = s.client.Get(baseURL) 102 | if err != nil { 103 | return fmt.Errorf("Failed to GET %q: %w", baseURL, err) 104 | } 105 | 106 | return nil 107 | }, 3) 108 | if err != nil { 109 | return "", err 110 | } 111 | 112 | defer resp.Body.Close() 113 | 114 | body, err := io.ReadAll(resp.Body) 115 | if err != nil { 116 | return "", fmt.Errorf("Failed to read body: %w", err) 117 | } 118 | 119 | // Look for .tar.xz 120 | selector := arch 121 | if variant != "" { 122 | selector = fmt.Sprintf("%s-%s", selector, variant) 123 | } 124 | 125 | regex := regexp.MustCompile(fmt.Sprintf(">void-%s-ROOTFS-.*.tar.xz<", selector)) 126 | 127 | // Find all rootfs related files 128 | matches := regex.FindAllString(string(body), -1) 129 | if len(matches) > 0 { 130 | // Take the first match since they're all the same anyway 131 | return strings.Trim(matches[0], "<>"), nil 132 | } 133 | 134 | return "", errors.New("Failed to find latest build") 135 | } 136 | -------------------------------------------------------------------------------- /windows/wiminfo.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | SupportedWindowsVersions = []string{ 14 | "w11", "w10", "w8", "w7", "2k19", "2k12", "2k16", 15 | "2k22", "2k25", "2k3", "2k8", "xp", "2k12r2", "2k8r2", "w8.1", 16 | } 17 | 18 | SupportedWindowsArchitectures = []string{ 19 | "amd64", "ARM64", "x86", 20 | } 21 | ) 22 | 23 | type WimInfo map[int]map[string]string 24 | 25 | func (info WimInfo) ImageCount() int { 26 | return len(info) - 1 27 | } 28 | 29 | func (info WimInfo) Name(index int) string { 30 | return info[index]["Name"] 31 | } 32 | 33 | func (info WimInfo) MajorVersion(index int) string { 34 | return info[index]["Major Version"] 35 | } 36 | 37 | func (info WimInfo) Architecture(index int) string { 38 | return info[index]["Architecture"] 39 | } 40 | 41 | type Aliases map[string][]string 42 | 43 | func (as Aliases) MatchString(desc string) string { 44 | for k, v := range as { 45 | for _, a := range v { 46 | if regexp.MustCompile(fmt.Sprintf("(?i)%s", a)).MatchString(desc) { 47 | return k 48 | } 49 | } 50 | } 51 | 52 | return "" 53 | } 54 | 55 | func ParseWimInfo(r io.Reader) (WimInfo, error) { 56 | scanner := bufio.NewScanner(r) 57 | nextSection := func() map[string]string { 58 | sect := map[string]string{} 59 | for scanner.Scan() { 60 | line := scanner.Text() 61 | if line == "" { 62 | break 63 | } 64 | 65 | idx := strings.IndexByte(line, ':') 66 | if idx == -1 { 67 | continue 68 | } 69 | 70 | key := strings.TrimSpace(line[:idx]) 71 | if key == "" { 72 | continue 73 | } 74 | 75 | val := strings.TrimSpace(line[idx+1:]) 76 | sect[key] = val 77 | } 78 | 79 | return sect 80 | } 81 | 82 | header := nextSection() 83 | count, err := strconv.Atoi(header["Image Count"]) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if count == 0 { 89 | err = fmt.Errorf("Failed to parse wim info") 90 | return nil, err 91 | } 92 | 93 | info := WimInfo{0: header} 94 | for i := 1; i <= count; i++ { 95 | index, section := 0, nextSection() 96 | index, err = strconv.Atoi(section["Index"]) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | if index != i { 102 | err = fmt.Errorf("Failed to parse wim info: %d != %d", index, i) 103 | return nil, err 104 | } 105 | 106 | info[i] = section 107 | } 108 | 109 | return info, nil 110 | } 111 | 112 | func DetectWindowsVersion(desc string) string { 113 | version := Aliases{ 114 | "2k12r2": {"2k12r2", "w2k12r2", "win2k12r2", "windows.?server.?2012?.r2"}, 115 | "2k8r2": {"2k8r2", "w2k8r2", "win2k8r2", "windows.?server.?2008?.r2"}, 116 | "w8.1": {"w8.1", "win8.1", "windows.?8.1"}, 117 | }.MatchString(desc) 118 | if version != "" { 119 | return version 120 | } 121 | 122 | return Aliases{ 123 | "w11": {"w11", "win11", "windows.?11"}, 124 | "w10": {"w10", "win10", "windows.?10"}, 125 | "w8": {"w8", "win8", "windows.?8"}, 126 | "w7": {"w7", "win7", "windows.?7"}, 127 | "2k19": {"2k19", "w2k19", "win2k19", "windows.?server.?2019"}, 128 | "2k12": {"2k12", "w2k12", "win2k12", "windows.?server.?2012"}, 129 | "2k16": {"2k16", "w2k16", "win2k16", "windows.?server.?2016"}, 130 | "2k22": {"2k22", "w2k22", "win2k22", "windows.?server.?2022"}, 131 | "2k25": {"2k25", "w2k25", "win2k25", "windows.?server.?2025"}, 132 | "2k3": {"2k3", "w2k3", "win2k3", "windows.?server.?2003"}, 133 | "2k8": {"2k8", "w2k8", "win2k8", "windows.?server.?2008"}, 134 | "xp": {"xp", "wxp", "winxp", "windows.?xp"}, 135 | }.MatchString(desc) 136 | } 137 | 138 | func DetectWindowsArchitecture(desc string) string { 139 | arch := Aliases{ 140 | "amd64": {"amd64", "x64", "x86_64"}, 141 | "ARM64": {"arm64", "aarch64"}, 142 | }.MatchString(desc) 143 | if arch != "" { 144 | return arch 145 | } 146 | 147 | return Aliases{ 148 | "x86": {"x86_32", "x86"}, 149 | }.MatchString(desc) 150 | } 151 | -------------------------------------------------------------------------------- /shared/osarch.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lxc/incus/v6/shared/osarch" 7 | ) 8 | 9 | var alpineLinuxArchitectureNames = map[int]string{ 10 | osarch.ARCH_32BIT_INTEL_X86: "x86", 11 | osarch.ARCH_64BIT_INTEL_X86: "x86_64", 12 | osarch.ARCH_32BIT_ARMV7_LITTLE_ENDIAN: "armv7", 13 | } 14 | 15 | var archLinuxArchitectureNames = map[int]string{ 16 | osarch.ARCH_64BIT_INTEL_X86: "x86_64", 17 | osarch.ARCH_32BIT_ARMV7_LITTLE_ENDIAN: "armv7", 18 | osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN: "aarch64", 19 | } 20 | 21 | var centosArchitectureNames = map[int]string{ 22 | osarch.ARCH_32BIT_INTEL_X86: "i386", 23 | } 24 | 25 | var debianArchitectureNames = map[int]string{ 26 | osarch.ARCH_32BIT_INTEL_X86: "i386", 27 | osarch.ARCH_64BIT_INTEL_X86: "amd64", 28 | osarch.ARCH_32BIT_ARMV7_LITTLE_ENDIAN: "armhf", 29 | osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN: "arm64", 30 | osarch.ARCH_32BIT_POWERPC_BIG_ENDIAN: "powerpc", 31 | osarch.ARCH_64BIT_POWERPC_BIG_ENDIAN: "powerpc64", 32 | osarch.ARCH_64BIT_POWERPC_LITTLE_ENDIAN: "ppc64el", 33 | } 34 | 35 | var gentooArchitectureNames = map[int]string{ 36 | osarch.ARCH_32BIT_INTEL_X86: "i686", 37 | osarch.ARCH_64BIT_INTEL_X86: "amd64", 38 | osarch.ARCH_32BIT_ARMV7_LITTLE_ENDIAN: "armv7a_hardfp", 39 | osarch.ARCH_32BIT_POWERPC_BIG_ENDIAN: "ppc", 40 | osarch.ARCH_64BIT_POWERPC_BIG_ENDIAN: "ppc64", 41 | osarch.ARCH_64BIT_POWERPC_LITTLE_ENDIAN: "ppc64le", 42 | osarch.ARCH_64BIT_S390_BIG_ENDIAN: "s390x", 43 | } 44 | 45 | var plamoLinuxArchitectureNames = map[int]string{ 46 | osarch.ARCH_32BIT_INTEL_X86: "x86", 47 | } 48 | 49 | var altLinuxArchitectureNames = map[int]string{ 50 | osarch.ARCH_32BIT_INTEL_X86: "i586", 51 | osarch.ARCH_64BIT_INTEL_X86: "x86_64", 52 | osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN: "aarch64", 53 | } 54 | 55 | var voidLinuxArchitectureNames = map[int]string{ 56 | osarch.ARCH_32BIT_INTEL_X86: "i686", 57 | osarch.ARCH_64BIT_INTEL_X86: "x86_64", 58 | osarch.ARCH_32BIT_ARMV7_LITTLE_ENDIAN: "armv7l", 59 | osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN: "aarch64", 60 | } 61 | 62 | var funtooArchitectureNames = map[int]string{ 63 | osarch.ARCH_32BIT_INTEL_X86: "generic_32", 64 | osarch.ARCH_64BIT_INTEL_X86: "generic_64", 65 | osarch.ARCH_32BIT_ARMV7_LITTLE_ENDIAN: "armv7a_vfpv3_hardfp", 66 | osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN: "arm64_generic", 67 | } 68 | 69 | var slackwareArchitectureNames = map[int]string{ 70 | osarch.ARCH_32BIT_INTEL_X86: "i586", 71 | osarch.ARCH_64BIT_INTEL_X86: "x86_64", 72 | } 73 | 74 | var distroArchitecture = map[string]map[int]string{ 75 | "alpinelinux": alpineLinuxArchitectureNames, 76 | "altlinux": altLinuxArchitectureNames, 77 | "archlinux": archLinuxArchitectureNames, 78 | "centos": centosArchitectureNames, 79 | "debian": debianArchitectureNames, 80 | "gentoo": gentooArchitectureNames, 81 | "plamolinux": plamoLinuxArchitectureNames, 82 | "voidlinux": voidLinuxArchitectureNames, 83 | "funtoo": funtooArchitectureNames, 84 | "slackware": slackwareArchitectureNames, 85 | } 86 | 87 | // GetArch returns the correct architecture name used by the specified 88 | // distribution. 89 | func GetArch(distro, arch string) (string, error) { 90 | // Special case armel as it is effectively a different userspace variant 91 | // of armv7 without hard-float and so doesn't have its own kernel architecture name 92 | if arch == "armel" { 93 | return "armel", nil 94 | } 95 | 96 | archMap, ok := distroArchitecture[distro] 97 | if !ok { 98 | return "unknown", fmt.Errorf("Architecture map isn't supported: %s", distro) 99 | } 100 | 101 | archID, err := osarch.ArchitectureID(arch) 102 | if err != nil { 103 | return "unknown", err 104 | } 105 | 106 | archName, exists := archMap[archID] 107 | if exists { 108 | return archName, nil 109 | } 110 | 111 | return arch, nil 112 | } 113 | -------------------------------------------------------------------------------- /doc/examples/sabayon.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | distribution: sabayon 3 | description: Sabayon Builder 4 | expiry: 30d 5 | variant: builder 6 | architecture: amd64 7 | 8 | source: 9 | downloader: docker-http 10 | url: sabayon/builder-amd64 11 | 12 | environment: 13 | clear_defaults: true 14 | variables: 15 | - key: "SHELL" 16 | value: "/bin/bash" 17 | - key: "ACCEPT_LICENSE" 18 | value: "*" 19 | - key: "ETP_NONINTERACTIVE" 20 | value: "1" 21 | 22 | targets: 23 | lxc: 24 | create_message: | 25 | You just created a Sabayon container (arch={{ image.architecture }}) 26 | 27 | config: 28 | - type: all 29 | before: 5 30 | content: |- 31 | lxc.include = LXC_TEMPLATE_CONFIG/sabayon.common.conf 32 | 33 | - type: user 34 | before: 5 35 | content: |- 36 | lxc.include = LXC_TEMPLATE_CONFIG/sabayon.userns.conf 37 | 38 | - type: all 39 | after: 4 40 | content: |- 41 | lxc.include = LXC_TEMPLATE_CONFIG/common.conf 42 | 43 | - type: user 44 | after: 4 45 | content: |- 46 | lxc.include = LXC_TEMPLATE_CONFIG/userns.conf 47 | 48 | - type: all 49 | content: |- 50 | lxc.arch = {{ image.architecture_kernel }} 51 | 52 | files: 53 | - path: /etc/hostname 54 | generator: hostname 55 | 56 | - path: /etc/hosts 57 | generator: hosts 58 | 59 | packages: 60 | manager: equo 61 | 62 | # repositories: 63 | # - name: "community" 64 | # type: "enman" 65 | # Enable main repository 66 | # - name: "sabayonlinux.org" 67 | # type: "equo" 68 | 69 | update: true 70 | cleanup: true 71 | 72 | sets: 73 | - packages: 74 | - sabayon-live 75 | action: install 76 | 77 | actions: 78 | # Spinbase image doesn't include enman tool 79 | # for external repositories. This is not needed 80 | # if external repository aren't used or it's used equ 81 | # as repo type. 82 | #- trigger: post-unpack 83 | # action: |- 84 | # #!/bin/sh 85 | # equo up && equo i enman 86 | 87 | - trigger: post-packages 88 | action: |- 89 | #!/bin/sh 90 | echo -5 | equo conf update 91 | 92 | # Disable systemd-remount-fs.service because 93 | # on unprivileged container systemd can't 94 | # remount filesystem. 95 | - trigger: post-packages 96 | action: |- 97 | #!/bin/sh 98 | cd /etc/systemd/system 99 | ln -s /dev/null systemd-remount-fs.service 100 | 101 | # Disable mount of hugepages 102 | - trigger: post-packages 103 | action: |- 104 | #!/bin/bash 105 | cd /etc/systemd/system 106 | ln -s /dev/null dev-hugepages.mount 107 | 108 | # Disable systemd-journald-audit service 109 | - trigger: post-packages 110 | action: |- 111 | #!/bin/bash 112 | cd /etc/systemd/system 113 | ln -s /dev/null systemd-journald-audit.socket 114 | 115 | # Disable sabayon-anti-fork-bomb limits 116 | # (already apply to host) 117 | - trigger: post-packages 118 | action: |- 119 | #!/bin/bash 120 | sed -i -e 's/^*/#*/g' /etc/security/limits.d/00-sabayon-anti-fork-bomb.conf 121 | sed -i -e 's/^root/#root/g' /etc/security/limits.d/00-sabayon-anti-fork-bomb.conf 122 | 123 | # Configure DHCP for interface eth0 by default. 124 | # Avoid to use DHCP for any interface to avoid reset of docker 125 | # interfaces or others custom interfaces. 126 | - trigger: post-packages 127 | action: |- 128 | #!/bin/bash 129 | cat > /etc/systemd/network/default_dhcp.network << "EOF" 130 | [Network] 131 | DHCP=ipv4 132 | 133 | [Match] 134 | Name=eth0 135 | 136 | [DHCP] 137 | UseDomains=true 138 | EOF 139 | 140 | # Enable systemd-networkd service by default. 141 | - trigger: post-packages 142 | action: |- 143 | #!/bin/bash 144 | systemctl enable systemd-networkd 145 | 146 | # Clean journal directory (to avoid permission errors) 147 | - trigger: post-packages 148 | action: |- 149 | rm -rf /var/log/journal/ 150 | 151 | mappings: 152 | architecture_map: debian 153 | 154 | -------------------------------------------------------------------------------- /managers/apt.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | incus "github.com/lxc/incus/v6/shared/util" 12 | 13 | "github.com/lxc/distrobuilder/shared" 14 | ) 15 | 16 | type apt struct { 17 | common 18 | } 19 | 20 | func (m *apt) load() error { 21 | m.commands = managerCommands{ 22 | clean: "apt-get", 23 | install: "apt-get", 24 | refresh: "apt-get", 25 | remove: "apt-get", 26 | update: "apt-get", 27 | } 28 | 29 | m.flags = managerFlags{ 30 | clean: []string{ 31 | "clean", 32 | }, 33 | global: []string{ 34 | "-y", 35 | }, 36 | install: []string{ 37 | "install", 38 | }, 39 | remove: []string{ 40 | "remove", "--auto-remove", 41 | }, 42 | refresh: []string{ 43 | "update", 44 | }, 45 | update: []string{ 46 | "dist-upgrade", 47 | }, 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (m *apt) manageRepository(repoAction shared.DefinitionPackagesRepository) error { 54 | var targetFile string 55 | 56 | if repoAction.Name == "sources.list" { 57 | targetFile = filepath.Join("/etc/apt", repoAction.Name) 58 | } else { 59 | targetFile = filepath.Join("/etc/apt/sources.list.d", repoAction.Name) 60 | 61 | if !strings.HasSuffix(targetFile, ".list") { 62 | targetFile = fmt.Sprintf("%s.list", targetFile) 63 | } 64 | } 65 | 66 | if !incus.PathExists(filepath.Dir(targetFile)) { 67 | err := os.MkdirAll(filepath.Dir(targetFile), 0o755) 68 | if err != nil { 69 | return fmt.Errorf("Failed to create directory %q: %w", filepath.Dir(targetFile), err) 70 | } 71 | } 72 | 73 | f, err := os.OpenFile(targetFile, os.O_CREATE|os.O_RDWR, 0o644) 74 | if err != nil { 75 | return fmt.Errorf("Failed to open file %q: %w", targetFile, err) 76 | } 77 | 78 | defer f.Close() 79 | 80 | content, err := io.ReadAll(f) 81 | if err != nil { 82 | return fmt.Errorf("Failed to read from file %q: %w", targetFile, err) 83 | } 84 | 85 | // Truncate file if it's not generated by distrobuilder 86 | if !strings.HasPrefix(string(content), "# Generated by distrobuilder\n") { 87 | err = f.Truncate(0) 88 | if err != nil { 89 | return fmt.Errorf("Failed to truncate %q: %w", targetFile, err) 90 | } 91 | 92 | _, err = f.Seek(0, 0) 93 | if err != nil { 94 | return fmt.Errorf("Failed to seek on file %q: %w", targetFile, err) 95 | } 96 | 97 | _, err = f.WriteString("# Generated by distrobuilder\n") 98 | if err != nil { 99 | return fmt.Errorf("Failed to write to file %q: %w", targetFile, err) 100 | } 101 | } 102 | 103 | _, err = f.WriteString(repoAction.URL) 104 | if err != nil { 105 | return fmt.Errorf("Failed to write to file %q: %w", targetFile, err) 106 | } 107 | 108 | // Append final new line if missing 109 | if !strings.HasSuffix(repoAction.URL, "\n") { 110 | _, err = f.WriteString("\n") 111 | if err != nil { 112 | return fmt.Errorf("Failed to write to file %q: %w", targetFile, err) 113 | } 114 | } 115 | 116 | if repoAction.Key != "" { 117 | var reader io.Reader 118 | 119 | if strings.HasPrefix(repoAction.Key, "-----BEGIN PGP PUBLIC KEY BLOCK-----") { 120 | reader = strings.NewReader(repoAction.Key) 121 | } else { 122 | // If only key ID is provided, we need gpg to be installed early. 123 | err := shared.RunCommand(m.ctx, nil, nil, "gpg", "--recv-keys", repoAction.Key) 124 | if err != nil { 125 | return fmt.Errorf("Failed to receive GPG keys: %w", err) 126 | } 127 | 128 | var buf bytes.Buffer 129 | 130 | err = shared.RunCommand(m.ctx, nil, &buf, "gpg", "--export", "--armor", repoAction.Key) 131 | if err != nil { 132 | return fmt.Errorf("Failed to export GPG keys: %w", err) 133 | } 134 | 135 | reader = &buf 136 | } 137 | 138 | signatureFilePath := filepath.Join("/etc/apt/trusted.gpg.d", fmt.Sprintf("%s.asc", repoAction.Name)) 139 | 140 | f, err := os.Create(signatureFilePath) 141 | if err != nil { 142 | return fmt.Errorf("Failed to create file %q: %w", signatureFilePath, err) 143 | } 144 | 145 | defer f.Close() 146 | 147 | _, err = io.Copy(f, reader) 148 | if err != nil { 149 | return fmt.Errorf("Failed to copy file: %w", err) 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /.sphinx/_static/version-switcher.js: -------------------------------------------------------------------------------- 1 | /* JavaScript for the _templates/variant-selector.html file, implementing 2 | * the version switcher for the documentation. 3 | * 4 | * The script gets available versions from the versions.json file on the 5 | * master branch (because the master branch contains the current information 6 | * on which versions we want to display). 7 | * It then links to other versions of the documentation - to the same page 8 | * if the page is available or to the index otherwise. 9 | */ 10 | 11 | // Link to the versions.json file on the master branch. 12 | var versionURL = "https://linuxcontainers.org/distrobuilder/docs/master/versions.json"; 13 | 14 | // URL prefix that is common for the different documentation sets. 15 | var URLprefix = "https://linuxcontainers.org/distrobuilder/docs/" 16 | 17 | 18 | 19 | $(document).ready(function() 20 | { 21 | 22 | // Read the versions.json file and call the listVersions function. 23 | var xhr = new XMLHttpRequest(); 24 | xhr.onreadystatechange = function () { 25 | if (xhr.readyState === 4) { 26 | if (xhr.status === 200) { 27 | listVersions(JSON.parse(xhr.responseText)); 28 | } 29 | else { 30 | console.log("URL "+versionURL+" cannot be loaded."); 31 | } 32 | } 33 | }; 34 | xhr.open('GET', versionURL, true); 35 | xhr.send(); 36 | 37 | }); 38 | 39 | // Retrieve the name of the current documentation set (for example, 40 | // 'master' or 'stable-5.0') and the path to the page (for example, 41 | // 'howto/pagename/'). 42 | function getPaths() 43 | { 44 | var paths = {}; 45 | 46 | var prefix = new URL(URLprefix); 47 | var url = window.location.pathname; 48 | 49 | if (url.startsWith(prefix.pathname)) { 50 | 51 | path = url.substr(prefix.pathname.length).split("/"); 52 | paths['current'] = path.shift(); 53 | if (paths['current'] == "master") { 54 | paths['current'] = "latest"; 55 | }; 56 | paths['page'] = path.join("/"); 57 | } 58 | else { 59 | console.log("Unexpected hosting URL!"); 60 | } 61 | 62 | return paths; 63 | 64 | } 65 | 66 | // Populate the version dropdown. 67 | function listVersions(data) 68 | { 69 | paths = getPaths(); 70 | 71 | var all_versions = document.getElementById("all-versions"); 72 | var current = document.getElementById("current"); 73 | for( var i = 0; i < data.length; i++ ) 74 | { 75 | var one = data[i]; 76 | if (one.id === paths['current']) { 77 | // Put the current version at the top without link. 78 | current.innerText = one.version+" ⌄"; 79 | } 80 | else { 81 | // Put other versions into the dropdown and link them to the 82 | // suitable URL. 83 | var version = document.createElement("a"); 84 | version.appendChild(document.createTextNode(one.version)); 85 | version.href = findNewURL(paths,one.id); 86 | all_versions.appendChild(version); 87 | } 88 | } 89 | } 90 | 91 | // Check if the same page exists in the other documentation set. 92 | // If yes, return the new link. Otherwise, link to the index page of 93 | // the other documentation set. 94 | function findNewURL(paths,newset) { 95 | 96 | var newURL = URLprefix.concat(newset,"/",paths['page']); 97 | var xhr = new XMLHttpRequest(); 98 | xhr.open('HEAD', newURL, false); 99 | xhr.send(); 100 | 101 | if (xhr.status == "404") { 102 | return URLprefix.concat(newset,"/"); 103 | } else { 104 | return newURL; 105 | } 106 | 107 | } 108 | 109 | // Toggle the version dropdown. 110 | function dropdown() { 111 | document.getElementById("all-versions").classList.toggle("show"); 112 | } 113 | 114 | // Close the dropdown menu if the user clicks outside of it. 115 | window.onclick = function(event) { 116 | if (!event.target.matches('.version_select')) { 117 | var dropdowns = document.getElementsByClassName("available_versions"); 118 | var i; 119 | for (i = 0; i < dropdowns.length; i++) { 120 | var openDropdown = dropdowns[i]; 121 | if (openDropdown.classList.contains('show')) { 122 | openDropdown.classList.remove('show'); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # distrobuilder 2 | System container and VM image builder for Incus and LXC. 3 | 4 | ## Status 5 | Type | Service | Status 6 | --- | --- | --- 7 | CI | GitHub | [![Build Status](https://github.com/lxc/distrobuilder/workflows/Tests/badge.svg)](https://github.com/lxc/distrobuilder/actions) 8 | Project status | CII Best Practices | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1728/badge)](https://bestpractices.coreinfrastructure.org/projects/1728) 9 | 10 | 11 | ## Command line options 12 | 13 | 14 | The following are the command line options of `distrobuilder`. You can use `distrobuilder` to create container images for both Incus and LXC. 15 | 16 | ```bash 17 | $ distrobuilder 18 | System container and VM image builder for Incus and LXC 19 | 20 | Usage: 21 | distrobuilder [command] 22 | 23 | Available Commands: 24 | build-dir Build plain rootfs 25 | build-incus Build Incus image from scratch 26 | build-lxc Build LXC image from scratch 27 | help Help about any command 28 | pack-incus Create Incus image from existing rootfs 29 | pack-lxc Create LXC image from existing rootfs 30 | repack-windows Repack Windows ISO with drivers included 31 | 32 | Flags: 33 | --cache-dir Cache directory 34 | --cleanup Clean up cache directory (default true) 35 | --debug Enable debug output 36 | --disable-overlay Disable the use of filesystem overlays 37 | -h, --help help for distrobuilder 38 | -o, --options Override options (list of key=value) 39 | -t, --timeout Timeout in seconds 40 | --version Print version number 41 | 42 | Use "distrobuilder [command] --help" for more information about a command. 43 | 44 | ``` 45 | 46 | 47 | 48 | ## Installing from package 49 | 50 | `distrobuilder` is available from the [Snap Store](https://snapcraft.io/distrobuilder). 51 | 52 | ``` 53 | sudo snap install distrobuilder --classic 54 | ``` 55 | 56 | ## Installing from source 57 | 58 | To compile `distrobuilder` from source, first install the Go programming language, and some other dependencies. 59 | 60 | - Debian-based: 61 | ``` 62 | sudo apt update 63 | sudo apt install -y golang-go gcc debootstrap rsync gpg squashfs-tools git make build-essential libwin-hivex-perl wimtools genisoimage 64 | ``` 65 | 66 | - ArchLinux-based: 67 | ``` 68 | sudo pacman -Syu 69 | sudo pacman -S go gcc debootstrap rsync gnupg squashfs-tools git make hivex cdrtools wimlib --needed 70 | ``` 71 | 72 | - Red Hat-based: 73 | ``` 74 | sudo dnf check-update 75 | sudo dnf install golang gcc debootstrap rsync gnupg2 squashfs-tools git make hivex genisoimage 76 | ``` 77 | 78 | NOTE: Distrobuilder requires Go 1.21 or higher, if your distribution doesn't have a recent enough version available, [get it from upstream](https://go.dev/doc/install). 79 | 80 | Second, download the source code of the `distrobuilder` repository (this repository). 81 | 82 | ``` 83 | mkdir -p $HOME/go/src/github.com/lxc/ 84 | cd $HOME/go/src/github.com/lxc/ 85 | git clone https://github.com/lxc/distrobuilder 86 | ``` 87 | 88 | Third, enter the directory with the source code of `distrobuilder` and run `make` to compile the source code. This will generate the executable program `distrobuilder`, and it will be located at `$HOME/go/bin/distrobuilder`. 89 | 90 | ``` 91 | cd ./distrobuilder 92 | make 93 | ``` 94 | 95 | Finally, you can run `distrobuilder` as follows. 96 | ``` 97 | $HOME/go/bin/distrobuilder 98 | ``` 99 | 100 | You may also add the directory `$HOME/go/bin/` to your $PATH so that you do not need to run the command with the full path. 101 | 102 | ## Runtime dependencies for building VM images 103 | 104 | If you intend to build Incus VM images (via `distrobuilder build-incus --vm`), 105 | your system will need certain tools installed: 106 | 107 | - Debian-based: 108 | ``` 109 | sudo apt update 110 | sudo apt install -y btrfs-progs dosfstools qemu-kvm 111 | ``` 112 | 113 | 114 | 115 | ## How to use 116 | 117 | See [How to use `distrobuilder`](doc/howto/build.md) for instructions. 118 | 119 | ## Troubleshooting 120 | 121 | See [Troubleshoot `distrobuilder`](doc/howto/troubleshoot.md). 122 | -------------------------------------------------------------------------------- /generators/dump_test.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/lxc/distrobuilder/shared" 13 | ) 14 | 15 | func TestDumpGeneratorRunLXC(t *testing.T) { 16 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 17 | require.NoError(t, err) 18 | 19 | rootfsDir := filepath.Join(cacheDir, "rootfs") 20 | 21 | setup(t, cacheDir) 22 | defer teardown(cacheDir) 23 | 24 | def := shared.Definition{ 25 | Targets: shared.DefinitionTarget{ 26 | LXC: shared.DefinitionTargetLXC{ 27 | CreateMessage: "message", 28 | }, 29 | }, 30 | } 31 | 32 | generator, err := Load("dump", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 33 | Path: "/hello/world", 34 | Content: "hello {{ targets.lxc.create_message }}", 35 | Pongo: true, 36 | }, def) 37 | require.IsType(t, &dump{}, generator) 38 | require.NoError(t, err) 39 | 40 | err = generator.RunLXC(nil, shared.DefinitionTargetLXC{ 41 | CreateMessage: "message", 42 | }) 43 | require.NoError(t, err) 44 | 45 | require.FileExists(t, filepath.Join(rootfsDir, "hello", "world")) 46 | 47 | var buffer bytes.Buffer 48 | file, err := os.Open(filepath.Join(rootfsDir, "hello", "world")) 49 | require.NoError(t, err) 50 | defer file.Close() 51 | 52 | _, err = io.Copy(&buffer, file) 53 | require.NoError(t, err) 54 | 55 | require.Equal(t, "hello message\n", buffer.String()) 56 | 57 | generator, err = Load("dump", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 58 | Path: "/hello/world", 59 | Content: "hello {{ targets.lxc.create_message }}", 60 | }, def) 61 | require.IsType(t, &dump{}, generator) 62 | require.NoError(t, err) 63 | 64 | err = generator.RunLXC(nil, shared.DefinitionTargetLXC{ 65 | CreateMessage: "message", 66 | }) 67 | require.NoError(t, err) 68 | 69 | require.FileExists(t, filepath.Join(rootfsDir, "hello", "world")) 70 | 71 | file, err = os.Open(filepath.Join(rootfsDir, "hello", "world")) 72 | require.NoError(t, err) 73 | defer file.Close() 74 | 75 | buffer.Reset() 76 | _, err = io.Copy(&buffer, file) 77 | require.NoError(t, err) 78 | 79 | require.Equal(t, "hello {{ targets.lxc.create_message }}\n", buffer.String()) 80 | } 81 | 82 | func TestDumpGeneratorRunIncus(t *testing.T) { 83 | cacheDir, err := os.MkdirTemp(os.TempDir(), "distrobuilder-test-") 84 | require.NoError(t, err) 85 | 86 | rootfsDir := filepath.Join(cacheDir, "rootfs") 87 | 88 | setup(t, cacheDir) 89 | defer teardown(cacheDir) 90 | 91 | def := shared.Definition{ 92 | Targets: shared.DefinitionTarget{ 93 | Incus: shared.DefinitionTargetIncus{ 94 | VM: shared.DefinitionTargetIncusVM{ 95 | Filesystem: "ext4", 96 | }, 97 | }, 98 | }, 99 | } 100 | 101 | generator, err := Load("dump", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 102 | Path: "/hello/world", 103 | Content: "hello {{ targets.incus.vm.filesystem }}", 104 | Pongo: true, 105 | }, def) 106 | require.IsType(t, &dump{}, generator) 107 | require.NoError(t, err) 108 | 109 | err = generator.RunIncus(nil, shared.DefinitionTargetIncus{ 110 | VM: shared.DefinitionTargetIncusVM{ 111 | Filesystem: "ext4", 112 | }, 113 | }) 114 | require.NoError(t, err) 115 | 116 | require.FileExists(t, filepath.Join(rootfsDir, "hello", "world")) 117 | 118 | var buffer bytes.Buffer 119 | file, err := os.Open(filepath.Join(rootfsDir, "hello", "world")) 120 | require.NoError(t, err) 121 | defer file.Close() 122 | 123 | _, err = io.Copy(&buffer, file) 124 | require.NoError(t, err) 125 | 126 | require.Equal(t, "hello ext4\n", buffer.String()) 127 | 128 | file.Close() 129 | 130 | generator, err = Load("dump", nil, cacheDir, rootfsDir, shared.DefinitionFile{ 131 | Path: "/hello/world", 132 | Content: "hello {{ targets.incus.vm.filesystem }}", 133 | }, def) 134 | require.IsType(t, &dump{}, generator) 135 | require.NoError(t, err) 136 | 137 | err = generator.RunIncus(nil, shared.DefinitionTargetIncus{ 138 | VM: shared.DefinitionTargetIncusVM{ 139 | Filesystem: "ext4", 140 | }, 141 | }) 142 | require.NoError(t, err) 143 | 144 | require.FileExists(t, filepath.Join(rootfsDir, "hello", "world")) 145 | 146 | file, err = os.Open(filepath.Join(rootfsDir, "hello", "world")) 147 | require.NoError(t, err) 148 | defer file.Close() 149 | 150 | buffer.Reset() 151 | _, err = io.Copy(&buffer, file) 152 | require.NoError(t, err) 153 | 154 | require.Equal(t, "hello {{ targets.incus.vm.filesystem }}\n", buffer.String()) 155 | } 156 | -------------------------------------------------------------------------------- /sources/archlinux-http.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "regexp" 11 | "sort" 12 | "strings" 13 | 14 | "github.com/antchfx/htmlquery" 15 | 16 | "github.com/lxc/distrobuilder/shared" 17 | ) 18 | 19 | type archlinux struct { 20 | common 21 | } 22 | 23 | // Run downloads an Arch Linux tarball. 24 | func (s *archlinux) Run() error { 25 | release := s.definition.Image.Release 26 | 27 | // Releases are only available for the x86_64 architecture. ARM only has 28 | // a "latest" tarball. 29 | if s.definition.Image.ArchitectureMapped == "x86_64" && release == "" { 30 | var err error 31 | 32 | // Get latest release 33 | release, err = s.getLatestRelease(s.definition.Source.URL, s.definition.Image.ArchitectureMapped) 34 | if err != nil { 35 | return fmt.Errorf("Failed to get latest release: %w", err) 36 | } 37 | } 38 | 39 | var fname string 40 | var tarball string 41 | 42 | switch s.definition.Image.ArchitectureMapped { 43 | case "x86_64": 44 | fname = fmt.Sprintf("archlinux-bootstrap-%s-%s.tar.zst", 45 | release, s.definition.Image.ArchitectureMapped) 46 | tarball = fmt.Sprintf("%s/%s/%s", s.definition.Source.URL, 47 | release, fname) 48 | case "riscv64": 49 | fname = "archriscv-latest.tar.zst" 50 | tarball = fmt.Sprintf("%s/images/%s", s.definition.Source.URL, fname) 51 | default: 52 | fname = fmt.Sprintf("ArchLinuxARM-%s-latest.tar.gz", 53 | s.definition.Image.ArchitectureMapped) 54 | tarball = fmt.Sprintf("%s/os/%s", s.definition.Source.URL, fname) 55 | } 56 | 57 | url, err := url.Parse(tarball) 58 | if err != nil { 59 | return fmt.Errorf("Failed to parse URL %q: %w", tarball, err) 60 | } 61 | 62 | if !s.definition.Source.SkipVerification && url.Scheme != "https" && 63 | len(s.definition.Source.Keys) == 0 { 64 | return errors.New("GPG keys are required if downloading from HTTP") 65 | } 66 | 67 | fpath, err := s.DownloadHash(s.definition.Image, tarball, "", nil) 68 | if err != nil { 69 | return fmt.Errorf("Failed to download %q: %w", tarball, err) 70 | } 71 | 72 | // Force gpg checks when using http 73 | if !s.definition.Source.SkipVerification && url.Scheme != "https" { 74 | _, err = s.DownloadHash(s.definition.Image, tarball+".sig", "", nil) 75 | if err != nil { 76 | return fmt.Errorf("Failed downloading %q: %w", tarball+".sig", err) 77 | } 78 | 79 | valid, err := s.VerifyFile( 80 | filepath.Join(fpath, fname), 81 | filepath.Join(fpath, fname+".sig")) 82 | if err != nil { 83 | return fmt.Errorf("Failed to verify %q: %w", fname, err) 84 | } 85 | 86 | if !valid { 87 | return fmt.Errorf("Invalid signature for %q", fname) 88 | } 89 | } 90 | 91 | s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image") 92 | 93 | // Unpack 94 | err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir) 95 | if err != nil { 96 | return fmt.Errorf("Failed to unpack file %q: %w", filepath.Join(fpath, fname), err) 97 | } 98 | 99 | // Move everything inside 'root.' (which was is the tarball) to its 100 | // parent directory 101 | files, err := filepath.Glob(fmt.Sprintf("%s/*", filepath.Join(s.rootfsDir, 102 | "root."+s.definition.Image.ArchitectureMapped))) 103 | if err != nil { 104 | return fmt.Errorf("Failed to get files: %w", err) 105 | } 106 | 107 | for _, file := range files { 108 | err = os.Rename(file, filepath.Join(s.rootfsDir, path.Base(file))) 109 | if err != nil { 110 | return fmt.Errorf("Failed to rename file %q: %w", file, err) 111 | } 112 | } 113 | 114 | path := filepath.Join(s.rootfsDir, "root."+s.definition.Image.ArchitectureMapped) 115 | 116 | err = os.RemoveAll(path) 117 | if err != nil { 118 | return fmt.Errorf("Failed to remove %q: %w", path, err) 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (s *archlinux) getLatestRelease(URL string, arch string) (string, error) { 125 | doc, err := htmlquery.LoadURL(URL) 126 | if err != nil { 127 | return "", fmt.Errorf("Failed to load URL %q: %w", URL, err) 128 | } 129 | 130 | re := regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}/?$`) 131 | 132 | var releases []string 133 | 134 | for _, node := range htmlquery.Find(doc, `//a[@href]/text()`) { 135 | if re.MatchString(node.Data) { 136 | releases = append(releases, strings.TrimSuffix(node.Data, "/")) 137 | } 138 | } 139 | 140 | if len(releases) == 0 { 141 | return "", errors.New("Failed to determine latest release") 142 | } 143 | 144 | // Sort releases in case they're out-of-order 145 | sort.Strings(releases) 146 | 147 | return releases[len(releases)-1], nil 148 | } 149 | --------------------------------------------------------------------------------