Right now you are viewing the "rendered" version of the markdown document.
If you want to download the "raw" version, go here.
- Making a virtual environment for
apt-get - Using
apt-getto discover dependencies - Downloading deb files and building the ISO file
- Adding more tools
- Installing packages with apt-cdrom
- Known issues and limitations
- Annex - The license
APT-Cdrom image builder
This article uses Literate Programming: it contains runnable source-code! To extract the code from the MarkDown File, use the tool presented in the A new spin on literate programming article.
This project is based on the script at https://github.com/mdukat/manual-apt-cdrom, but completely rewritten in Ruby, annotated with explanations along the way.
{"filename": "apt-cdrom-image-builder.rb"}
#!/usr/bin/env ruby
<<spdx_headers>>
require 'open3'
require 'tmpdir'
require 'fileutils'
<<set_country_code>>
# Accumulator for ubuntu package-names
wanted_packages = Array.new
tools = ['vim', 'htop', 'genisoimage', 'kas', 'checkinstall', 'brasero',
'glances', 'bmaptool', 'gparted', 'net-tools']
wanted_packages += tools
<<want_more_packages>>
# Deduplicate the accumulator
wanted_packages.uniq!
<<virtual_aptget_use>>
Making a virtual environment for apt-get
Dir.mktmpdir
is a useful Ruby method, that creates a temporary directory, passes it
to a block of code, and when the block of code finishes (with success or with
an exception), it erases the contents of that directory. The directory is
typically created inside the /tmp path.
This means that we don't need to worry about deleting objects on the
filesystem, since most of the time it happens automatically (but there are
situations when it cannot be done, such as when the application receives an
SIGKILL signal and exits immediately). The only file created outside of this
tmp_dir is the final ISO image, called repo.iso.
{"name": "virtual_aptget_use"}
Dir.mktmpdir do |tmp_dir|
puts "Working with dir #{tmp_dir}."
<<apt_get_venv>>
<<apt_init_discover_dependencies>>
<<apt_download_and_build_iso>>
end
The apt.conf file
We use a separate apt.conf file to override the system-level apt configuration.
This way we separate the configurations used by the system itself, from the cdrom-image-builder.
apt.conf key |
Typical value | Updated value |
|---|---|---|
Dir::State |
/var/lib/apt |
#{tmp_dir}/apt |
Dir::State::status |
/var/lib/dpkg/status |
#{tmp_dir}/dpkg/status |
Dir::Etc::SourceList |
/etc/apt/sources.list |
#{tmp_dir}/sources.list |
Dir::Etc::SourceParts |
/etc/apt/sources.list.d/ |
none |
Dir::Cache |
/var/cache/apt |
#{tmp_dir}/cache |
Dir::Etc::Preferences |
/etc/apt/preferences |
#{tmp_dir}/preferences |
{"name": "apt_conf"}
File.write(
"#{tmp_dir}/apt.conf",
<<~HEREDOC
Dir::State "#{tmp_dir}/apt";
Dir::State::status "#{tmp_dir}/dpkg/status";
Dir::Etc::SourceList "#{tmp_dir}/sources.list";
Dir::Etc::SourceParts "none";
Dir::Cache "#{tmp_dir}/cache";
<<apt_setup_preferences>>
pkgCacheGen::Essential "none";
# apt-secure(8)
Acquire::AllowInsecureRepositories "true";
# apt-get
APT::Get::AllowUnauthenticated "true";
HEREDOC
)
To use this file at #{tmp_dir}/apt.conf, whenever we need to call
a tool like apt-get, we must specify it as:
apt-get -c ${tmp_dir}/apt.conf install firefox
The sources.list file
Feel free to replace the mirror:
{"name": "set_country_code"}
# Used to select the nearest mirror
COUNTRY_CODE = 'pl'
We initialize the #{tmpdir}/sources.list configuration file with the official repositories for the current distribution.
{"name": "apt_sources_list"}
File.write(
"#{tmpdir}/sources.list",
<<~HEREDOC
deb http://#{COUNTRY_CODE}.archive.ubuntu.com/ubuntu/ resolute-backports main restricted universe multiverse
deb http://#{COUNTRY_CODE}.archive.ubuntu.com/ubuntu/ resolute main restricted universe multiverse
deb http://#{COUNTRY_CODE}.archive.ubuntu.com/ubuntu/ resolute-updates main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu resolute-security main restricted universe multiverse
<<firefox_repo_list>>
<<vscode_repo_list>>
HEREDOC
)
Misc
FileUtils.mkdir_p and
FileUtils.touch methods are used to initialize empty directories and files.
{"name": "apt_get_venv"}
<<apt_conf>>
<<apt_sources_list>>
FileUtils.mkdir_p [
"#{tmp_dir}/cache",
"#{tmp_dir}/dpkg",
"#{tmp_dir}/apt/partial",
"#{tmp_dir}/apt/archives/partial",
"#{tmp_dir}/rootrepo/repo"
]
FileUtils.touch "#{tmp_dir}/dpkg/status"
<<firefox_preference>>
Using apt-get to discover dependencies
apt-get update is used to "learn" what packages
are available in the upstream repositories, and what are their dependencies.
Because we're working in an empty "virtual environment" for apt, must first
download these details.
Then, apt-cache depends is used to determine what packages are needed
in order to install wanted_packages. This explores the dependency graph
and shows the full list of required packages.
When using system,
we specify the exception: true parameter: if the command fails, a Ruby
exception is raised, and the entire script crashes, while also cleaning up
the contents of the tmp_dir. Without this option, we would need to
validate the return-code of the call.
Also, we use Open3.capture2
to get in a variable the standard-output of the command.
{"name": "apt_init_discover_dependencies"}
system(
"apt-get -c #{tmp_dir}/apt.conf update",
{ exception: true }
)
apt_cache_out, apt_cache_return = Open3.capture2(
"apt-cache -c #{tmp_dir}/apt.conf " \
'depends --recurse ' \
'--no-recommends --no-suggests --no-conflicts --no-breaks ' \
'--no-replaces --no-enhances --no-pre-depends ' \
wanted_packages.join(' ')
)
mirrored_packages = apt_cache_out.lines(chomp: true)
.delete_if { |it| it.start_with? ' ' }
.delete_if { |it| it.start_with? '<' }
At this point we have in mirrored_packages the list of packages that
must be downloaded: it includes the wanted_packages and all
of their transitive dependencies.
References:
Downloading deb files and building the ISO file
We call apt-get download using the "virtual environment" (by forcing the
use of the special config-file with -c #{tmp_dir}/apt.conf) and with the
list of packages that we want downloaded to the current directory.
Here we use chdir to override the current working directory
seen by the called command.
{"name": "apt_download_and_build_iso"}
iso_root_dir = "#{tmp_dir}/rootrepo"
system(
"apt-get -c #{tmp_dir}/apt.conf download #{mirrored_packages.join(' ')}",
{
chdir: "#{iso_root_dir}/repo",
exception: true
}
)
<<set_up_mirror>>
<<make_iso>>
At this point we have downloaded everything in the
#{iso_root_dir}/repo directory, and we need to produce some
a metadata-file containing the list of packages present in this
mirror. We use the dpkg-scanpackages
tool for this.
{"name": "set_up_mirror"}
system(
"dpkg-scanpackages #{iso_root_dir}/repo /dev/null | gzip -9c > #{iso_root_dir}/Packages.gz",
{ exception: true }
)
Now that the deb files and the file containing the metadata (Packages.gz) are
in the #{iso_root_dir}, we can archive the entire thing into an ISO file
(repo.iso) placed in the local directory.
{"name": "make_iso"}
system(
"mkisofs -lJR -o repo.iso #{iso_root_dir}",
{ exception: true }
)
References:
- https://askubuntu.com/a/458754
- https://gist.github.com/awesomebytes/ce0643c1ddead589ab06e2a1e4c5861b
- https://unix.stackexchange.com/a/274751
Adding more tools
Now that the basic application has been defined, we can add
more tools in the image, by appending groups of elements to the
wanted_packages array. Every user has different needs, so
feel free to customize the list of packages.
Since the list is deduplicated at the end (by doing wanted_packages.uniq!),
you don't need to worry about adding the same package twice.
If you want to make sure what's the correct name of a package, visit https://packages.ubuntu.com/.
{"name": "want_more_packages"}
ruby = ['ruby', 'ruby-dev', 'ruby-bundler', 'libyaml-dev', 'libffi-dev',
'rbenv']
wanted_packages += ruby
yocto_prerequisites = ['build-essential', 'chrpath', 'cpio', 'debianutils',
'diffstat', 'file', 'gawk', 'gcc', 'git', 'iputils-ping',
'libacl1', 'locales', 'python3', 'python3-git',
'python3-jinja2', 'python3-pexpect', 'python3-pip',
'python3-submit', 'socat', 'texinfo', 'unzip', 'wget',
'xz-utils', 'zstd']
wanted_packages += yocto_prerequisites
fuse_archive_prerequisites = ['git', 'libboost-container-dev', 'libfuse3-dev',
'libarchive-dev', 'g++', 'pkg-config', 'make',
'gtest-dev', 'pandoc']
wanted_packages += fuse_archive_prerequisites
<<want_firefox>>
<<want_vscode>>
Adding VS Code
VS Code is not distributed in the official Ubuntu repositories, so in order to download it, we have two steps to follow:
- Adding the Microsoft repository to the
#{tmpdir}/sources.listfile:
{"name": "vscode_repo_list"}
deb [trusted=yes] https://packages.microsoft.com/repos/code stable main
- Adding the
codepackage-name to thewanted_packageslist.
{"name": "want_vscode"}
vscode = ['code']
wanted_packages += vscode
Adding Firefox
This is more complicated, because in the official Ubuntu distribution, firefox
is packaged as a snap, so apt-get doesn't download a regular deb package.
Fortunately, we can get the firefox deb package from the Mozilla repository,
so we repeat the same steps:
- Adding the Mozilla repository to the
#{tmpdir}/sources.listfile:
{"name": "firefox_repo_list"}
deb [trusted=yes] https://packages.mozilla.org/apt mozilla main
- Adding the
firefoxpackage-name to thewanted_packageslist.
{"name": "want_firefox"}
browser = ['firefox']
wanted_packages += browser
If we stopped here, apt-get would have a choice between two firefox
packages: one from the Mozilla repo and one from the Ubuntu repo (and it will
pick the Ubuntu repo, since it's the official upstream source). We need
to also change the priorities and preferences used by apt-get:
- First, we set indicate in the
#{tmp_dir}/apt.confconfig-file that we have a dedicated file recording our preferences at#{tmp_dir}/preferences.
{"name": "apt_setup_preferences"}
Dir::Etc::Preferences "#{tmp_dir}/preferences";
- Then, we write in this file that we like the
packages.mozilla.orgrepository more than the default one.
{"name": "firefox_preference"}
File.write(
"#{tmp_dir}/preferences",
<<~HEREDOC
Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000
HEREDOC
)
Installing packages with apt-cdrom
First use of apt-cdrom
Edit the /etc/apt/apt.conf file to add a line with:
Acquire::cdrom::mount "/media/cdrom/";
There's a bug first reported in 2010 where the apt-cdrom
tool gets confused because it comes pre-packaged with two paths to use when
loading packages from optical media: /media/cdrom and /media/apt.
This workaround clears out the confusion, elimiating the references to
/media/apt.
References:
- https://bugs-devel.debian.org/cgi-bin/bugreport.cgi?bug=606930
- https://www.linuxquestions.org/questions/debian-26/problems-with-apt-cdrom-add-command-866761/
Normal use
apt-cdrom add
# edit the /etc/apt/sources.list file
# to add [trusted=yes] label
# Forget the packages found in "online" repositories.
sudo rm -rf /var/lib/apt/lists/*
# Discover the packages found on the optical medium.
sudo apt-get update
# Install a package
sudo apt-get install firefox
Needs to be set to "trusted":
Known issues and limitations
- The machine building the ISO file should have the same CPU architecture and OS version as the users of the CD/DVD.
- Although the script was made for Ubuntu 26.04 LTS Resolute, it will probably work for other Ubuntu & Debian-like distributions.
Annex - The license
This project attepts to be mostly-compliant with the REUSE Software specifications.
One requirement of the specification is to have the text of the chosen license (MIT in our case) in a file in the LICENSES directory. As such, we add it in:
{"filename": "LICENSES/MIT.txt"}
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
Another prerequisite of the specification is that every copyrightable file must include two comments using a special formatting, containing the license and the author/copyright holder.
{"name": "spdx_headers"}
# SPDX-FileCopyrightText: 2026 PersonalCompute.Net <publisher@PersonalCompute.Net>
# SPDX-License-Identifier: MIT
We can check the REUSE compliance using the lint tool:
pipx run reuse lint