aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2023-03-14 09:32:22 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2023-03-17 08:58:33 +0200
commitafd0b8699b009b96be34ba2a20441ecb223957ce (patch)
tree7b34b0166c994769135b2b10b4f885e0ad124a4e
parent3cc61ab4a6275428520cb0fb2f18dc3e09aef28a (diff)
Add support for generating installation archives in pkg-bindist
-rw-r--r--bpkg/buildfile11
-rw-r--r--bpkg/pkg-bindist.cli220
-rw-r--r--bpkg/pkg-bindist.cxx14
-rw-r--r--bpkg/system-package-manager-archive.cxx744
-rw-r--r--bpkg/system-package-manager-archive.hxx54
-rw-r--r--bpkg/system-package-manager-debian.cxx7
-rw-r--r--bpkg/system-package-manager-fedora.cxx7
-rw-r--r--bpkg/system-package-manager.cxx29
-rw-r--r--bpkg/system-package-manager.hxx5
-rwxr-xr-xdoc/cli.sh4
10 files changed, 1063 insertions, 32 deletions
diff --git a/bpkg/buildfile b/bpkg/buildfile
index 3ba9ea6..05ded7e 100644
--- a/bpkg/buildfile
+++ b/bpkg/buildfile
@@ -201,7 +201,7 @@ if $cli.configured
--generate-vector-scanner --generate-file-scanner --generate-group-scanner \
--keep-separator --generate-specifier --generate-parse --generate-merge \
--page-usage 'bpkg::print_$name$_' --ansi-color --ascii-tree \
---include-base-last --suppress-undocumented --option-length 24
+--include-base-last --suppress-undocumented --option-length 25
# Both --*-usage options.
#
@@ -212,11 +212,12 @@ if $cli.configured
cli.options += --long-usage # All other pages -- long usage.
- cli.cxx{pkg-build-options}: cli.options += --class-doc \
-bpkg::pkg_build_pkg_options=exclude-base --generate-modifier
+ cli.cxx{pkg-build-options}: cli.options += --generate-modifier \
+--class-doc bpkg::pkg_build_pkg_options=exclude-base
- cli.cxx{pkg-bindist-options}: cli.options += --class-doc \
-bpkg::pkg_bindist_debian_options=exclude-base
+ cli.cxx{pkg-bindist-options}: cli.options += \
+--class-doc bpkg::pkg_bindist_debian_options=exclude-base \
+--class-doc bpkg::pkg_bindist_archive_options=exclude-base
# Avoid generating CLI runtime and empty inline file for help topics.
#
diff --git a/bpkg/pkg-bindist.cli b/bpkg/pkg-bindist.cli
index f3c3899..cdbb10c 100644
--- a/bpkg/pkg-bindist.cli
+++ b/bpkg/pkg-bindist.cli
@@ -1,6 +1,8 @@
// file : bpkg/pkg-bindist.cli
// license : MIT; see accompanying LICENSE file
+include <map>;
+
include <bpkg/configuration.cli>;
"\section=1"
@@ -155,7 +157,7 @@ namespace bpkg
If empty value is specified, then no build metadata is included. By
default, the build metadata is the \cb{ID} and \cb{VERSION_ID}
components from \cb{os-release(5)}, for example, \cb{debian10} in
- version \cb{1.2.3-0~debian10}."
+ version \cb{1.2.3-0~debian10}. See also \cb{--os-release-*}."
}
string --debian-section
@@ -218,11 +220,194 @@ namespace bpkg
}
};
+ class pkg_bindist_archive_options
+ {
+ "\h|ARCHIVE DESCRIPTION|
+
+ The installation archive binary packages are generated by invoking the
+ \cb{build2} build system on the required packages directly in their
+ \cb{bpkg} configuration locations and installing them into the binary
+ package directory using the \cb{config.install.chroot} mechanism. Then
+ this directory is packaged with \cb{tar} or \cb{zip} to produce one or
+ more binary package archives. The installation directory layout and
+ the package archives to generate can be specified with the
+ \cb{--archive-install-*} and \cb{--archive-type} options (also refer
+ to their documentation for defaults).
+
+ The binary package directory (the top-level directory inside the
+ archive) as well as the archive file base (the file name without
+ the extension) are the same and have the following form:
+
+ \c{\i{package}-\i{version}-\i{build_metadata}}
+
+ Where \ci{package} is the package name and \ci{version} is the \cb{bpkg}
+ package version. Unless overridden with the \cb{--archive-build-meta}
+ option, \ci{build_metadata} has the following form:
+
+ \c{\i{cpu}-\i{os}[-\i{langrt}...]}
+
+ Where \ci{cpu} is the target CPU (for example, \cb{x86_64}), \ci{os} is
+ the \cb{ID} and \cb{VERSION_ID} components from \cb{os-release(5)} (or
+ equivalent, for example, \cb{debian11} or \cb{windows10}), and
+ \ci{langrt} are the language runtimes as mapped by the
+ \cb{--archive-lang*} options (for example, \cb{gcc12}, \cb{msvc17.4}).
+
+ For example, given the following invocation on Debian 11:
+
+ \
+ bpkg build libhello
+ bpkg test libhello
+ bpkg bindist \
+ -o /tmp/output/ \
+ --distribution=archive \
+ --archive-lang cc=gcc12 \
+ libhello
+ \
+
+ We will end up with the package archive in the following form:
+
+ \
+ libhello-1.2.3-x86_64-debian11-gcc12.tar.xz
+ \
+
+ The recommended language runtime id format is the runtime name followed
+ by the version, for example, \cb{gcc12} or \cb{msvc17.4}. Note that its
+ purpose is not to provide a precise specification of requirements but
+ rather to help the user of a binary package to pick the appropriate
+ variant. Refer to the \cb{--archive-lang*} options documentation for
+ details on the mapping semantics.
+
+ Instead of mapping languages individually you can specify entire build
+ metadata as a single value with the \cb{--archive-build-meta}, for
+ example:
+
+ \
+ bpkg bindist \
+ -o /tmp/output/ \
+ --distribution=archive \
+ --archive-build-meta=x86_64-linux-glibc
+ libhello
+ \
+
+ This will produce the package archive in the following form:
+
+ \
+ libhello-1.2.3-x86_64-linux-glibc.tar.xz
+ \
+
+ To install the binary package from archive simply unpack it using
+ \cb{tar} or \cb{zip}. You can use the \cb{--strip-components} \cb{tar}
+ option to remove the top-level package directory (the same can be
+ achieved for \cb{zip} archives by using \cb{bsdtar} on Windows). For
+ example, to unpack the package contents so that they end up in
+ \cb{/usr/local/}:
+
+ \
+ sudo tar -xf libhello-1.2.3-x86_64-debian11-gcc12.tar.xz \
+ -C / --strip-components=1
+ \
+
+ The installation archive package can be generated for a target other than
+ the host by specifying the target triplet with the \cb{--architecture}
+ option. In this case the \cb{bpkg} configuration is assumed to be
+ appropriately configured for cross-compiling to the specified target. You
+ will also need to explicitly specify the \cb{--archive-install-root}
+ option (or \cb{--archive-install-config}) as well as the
+ \cb{--os-release-id} option (and likely want to specify other
+ \cb{--os-release-*} options). For example, for cross-compiling from Linux
+ to Windows using the MinGW GCC toolchain:
+
+ \
+ bpkg bindist \
+ --distribution=archive \
+ --architecture=x86_64-w64-mingw32 \
+ --os-release-id=windows \
+ --os-release-name=Windows \
+ --os-release-version-id=10 \
+ --archive-install-root / \
+ --archive-lang cc=mingw_w64_gcc12 \
+ ...
+ \
+ "
+
+ "\h|PKG-BINDIST ARCHIVE OPTIONS|"
+
+ bool --archive-prepare-only
+ {
+ "Prepare all the package contents but do not create the binary package
+ archive, printing its directory instead unless requested to be quiet.
+ Implies \cb{--keep-output}."
+ }
+
+ strings --archive-type
+ {
+ "<ext>",
+ "Archive type to create specified as a file extension, for example,
+ \cb{tar.xz}, \cb{tar.gz}, \cb{tar}, \cb{zip}. Repeat this option to
+ generate multiple archive types. If unspecified, then a default type
+ appropriate for the target operating system is used, currently \cb{zip}
+ for Windows and \cb{tar.xz} for POSIX. Note, however, that these
+ defaults may change in the future."
+ }
+
+ std::map<string, string> --archive-lang
+ {
+ "<ln>=<rt>",
+ "Map interface language name <ln> to runtime id <rt>. If no mapping is
+ found for an interface language in this map, then fallback to the
+ \cb{--archive-lang-impl} map. If still no mapping is found, then
+ fail. If the information about an interface language is unimportant and
+ should be ignored, then empty runtime id can be specified. Note that
+ the mapping specified with this option is only considered if the
+ package type is a library (for other package types all languages
+ used are implementation)."
+ }
+
+ std::map<string, string> --archive-lang-impl
+ {
+ "<ln>=<rt>",
+ "Map implementation language name <ln> to runtime id <rt>. If no mapping
+ is found for an implementation language in this map, then assume
+ the information about this implementation language is unimportant
+ and ignore it (examples of such cases include static linking as well
+ as a language runtime that is always present)."
+ }
+
+ string --archive-build-meta
+ {
+ "<data>",
+ "Alternative build metadata to include after the version in the binary
+ package directory and file names. If empty value is specified, then no
+ build metadata is included."
+ }
+
+ dir_path --archive-install-root
+ {
+ "<d>",
+ "Alternative installation root directory. The default is \cb{/usr/local/}
+ on POSIX and \c{\b{C:\\}\i{project}\b{\\}} on Windows, where
+ \ci{project} is the \l{bpkg#manifest-package-project \cb{project}}
+ package manifest value."
+ }
+
+ bool --archive-install-config
+ {
+ "Use the installation directory layout (\cb{config.install.*} variables)
+ as configured instead of overriding them with defaults appropriate for
+ the target operating system. Note that this includes
+ \cb{config.install.private} and \cb{config.bin.rpath} if needed for a
+ private installation. Note also that the \cb{config.install.root} value
+ is still overridden with the \cb{--archive-install-root} option value
+ if specified."
+ }
+ };
+
// NOTE: remember to add the corresponding `--class-doc ...=exclude-base`
// (both in bpkg/ and doc/) if adding a new base class.
//
class pkg_bindist_options: configuration_options,
- pkg_bindist_debian_options
+ pkg_bindist_debian_options,
+ pkg_bindist_archive_options
{
"\h|PKG-BINDIST COMMON OPTIONS|"
@@ -230,10 +415,11 @@ namespace bpkg
{
"<name>",
"Alternative system/distribution package manager to generate the binary
- package for. The valid <name> values are \cb{debian} (Debian and
- alike, such as Ubuntu, etc) and \cb{fedora} (Fedora and alike,
- such as RHEL, CentOS, etc). Note that some package managers may
- only be supported when running on certain host operating systems."
+ package for. The valid <name> values are \cb{debian} (Debian and alike,
+ such as Ubuntu, etc), \cb{fedora} (Fedora and alike, such as RHEL,
+ CentOS, etc), and \cb{archive} (installation archive on any operating
+ system). Note that some package managers may only be supported when
+ running on certain host operating systems."
}
string --architecture
@@ -293,6 +479,28 @@ namespace bpkg
used to generate the binary package. This is primarily useful for
troubleshooting."
}
+
+ string --os-release-id
+ {
+ "<v>",
+ "Override the \cb{ID} component in \cb{os-release(5)} or equivalent.
+ Note that unlike the rest of the \cb{--os-release-*} options, this
+ option suppresses automatic detection of the host operating system
+ inormation."
+ }
+
+ string --os-release-version-id
+ {
+ "<v>",
+ "Override the \cb{VERSION_ID} component in \cb{os-release(5)} or
+ equivalent."
+ }
+
+ string --os-release-name
+ {
+ "<v>",
+ "Override the \cb{NAME} component in \cb{os-release(5)} or equivalent."
+ }
};
"
diff --git a/bpkg/pkg-bindist.cxx b/bpkg/pkg-bindist.cxx
index e3ec9fa..32be5dd 100644
--- a/bpkg/pkg-bindist.cxx
+++ b/bpkg/pkg-bindist.cxx
@@ -394,7 +394,11 @@ namespace bpkg
fail << "no standard distribution package manager for this host "
<< "or it is not yet supported" <<
info << "consider specifying alternative distribution package "
- << "manager with --distribution";
+ << "manager with --distribution" <<
+ info << "specify --distribution=archive to generate installation "
+ << "archive" <<
+ info << "consider specifying --os-release-* if unable to correctly "
+ << "auto-detect host operating system";
}
// Note that we pass type from here in case one day we want to provide an
@@ -412,8 +416,12 @@ namespace bpkg
diag_record dr (text);
- dr << "generated " << spm->os_release.name_id << " package for "
- << p.name << '/' << p.version << ':';
+ const string& d (o.distribution_specified ()
+ ? o.distribution ()
+ : spm->os_release.name_id);
+
+ dr << "generated " << d << " package for " << p.name << '/' << p.version
+ << ':';
for (const path& p: r)
dr << "\n " << p;
diff --git a/bpkg/system-package-manager-archive.cxx b/bpkg/system-package-manager-archive.cxx
new file mode 100644
index 0000000..a65d6aa
--- /dev/null
+++ b/bpkg/system-package-manager-archive.cxx
@@ -0,0 +1,744 @@
+// file : bpkg/system-package-manager-archive.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <bpkg/system-package-manager-archive.hxx>
+
+#include <bpkg/diagnostics.hxx>
+
+#include <bpkg/pkg-bindist-options.hxx>
+
+using namespace butl;
+
+namespace bpkg
+{
+ system_package_manager_archive::
+ system_package_manager_archive (bpkg::os_release&& osr,
+ const target_triplet& h,
+ string a,
+ optional<bool> progress,
+ const pkg_bindist_options* options)
+ : system_package_manager (move (osr), h, "", progress), ops (options)
+ {
+ if (!a.empty ())
+ {
+ assert (ops != nullptr);
+
+ try
+ {
+ target = target_triplet (a);
+ }
+ catch (const invalid_argument& e)
+ {
+ fail << "invalid --architecture target triplet value '" << a << "': "
+ << e;
+ }
+
+ if (!ops->os_release_id_specified ())
+ fail << "--architecture requires explict --os-release-id";
+
+ if (!ops->archive_install_root_specified () &&
+ !ops->archive_install_config ())
+ fail << "--architecture requires explict --archive-install-root";
+ }
+ else
+ target = host;
+
+ arch = target.cpu; // Set in case queried by someone else.
+ }
+
+ // env --chdir=<root> tar|zip ... <base>.<ext> <base>
+ //
+ // Return the archive file path.
+ //
+ static path
+ archive (const dir_path& root,
+ const string& base,
+ const string& e /* ext */)
+ {
+ // NOTE: similar code in build2 (libbuild2/dist/operation.cxx).
+
+ path an (base + '.' + e);
+ path ap (root / an);
+
+ // Use zip for .zip archives. Also recognize and handle a few well-known
+ // tar.xx cases (in case tar doesn't support -a or has other issues like
+ // MSYS). Everything else goes to tar in the auto-compress mode (-a).
+ //
+ // Note also that we pass the archive path as name (an) instead of path
+ // (ap) since we are running from the root directory (see below).
+ //
+ cstrings args;
+
+ // Separate compressor (gzip, xz, etc) state.
+ //
+ size_t i (0); // Command line start or 0 if not used.
+ auto_rmfile out_rm; // Output file cleanup (must come first).
+ auto_fd out_fd; // Output file.
+
+ if (e == "zip")
+ {
+ // On Windows we use libarchive's bsdtar (zip is an MSYS executable).
+ //
+ // While not explicitly stated, the compression-level option works
+ // for zip archives.
+ //
+#ifdef _WIN32
+ args = {"bsdtar",
+ "-a", // -a with the .zip extension seems to be the only way.
+ "--options=compression-level=9",
+ "-cf", an.string ().c_str (),
+ base.c_str (),
+ nullptr};
+#else
+ args = {"zip",
+ "-9",
+ "-rq", an.string ().c_str (),
+ base.c_str (),
+ nullptr};
+#endif
+ }
+ else
+ {
+ // On Windows we use libarchive's bsdtar with auto-compression (tar
+ // itself and quite a few compressors are MSYS executables).
+ //
+ // OpenBSD tar does not support --format but it appear ustar is the
+ // default (while this is not said explicitly in tar(1), it is said in
+ // pax(1) and confirmed on the mailing list). Nor does it support -a, at
+ // least as of 7.1 but we will let this play out naturally, in case this
+ // support gets added.
+ //
+ // Note also that in the future we may switch to libarchive in order to
+ // generate reproducible archives.
+ //
+ const char* l (nullptr); // Compression level (option).
+
+#ifdef _WIN32
+ args = {"bsdtar", "--format", "ustar"};
+
+ if (e == "tar.gz" || e == "tar.xz")
+ l = "--options=compression-level=9";
+#else
+ args = {"tar"
+#ifndef __OpenBSD__
+ , "--format", "ustar"
+#endif
+ };
+
+ // For gzip it's a good idea to use -9 by default. While for xz, -9 is
+ // not recommended as the default due memory requirements, in our case
+ // (large binary archives on development machines), this is unlikely to
+ // be an issue.
+ //
+ // Note also that the compression level can be altered via the GZIP
+ // (GZIP_OPT also seems to work) and XZ_OPT environment variables,
+ // respectively.
+ //
+ const char* c (nullptr);
+
+ if (e == "tar.gz") { c = "gzip"; l = "-9"; }
+ else if (e == "tar.xz") { c = "xz"; l = "-9"; }
+
+ if (c != nullptr)
+ {
+ args.push_back ("-cf");
+ args.push_back ("-");
+ args.push_back (base.c_str ());
+ args.push_back (nullptr); i = args.size ();
+ args.push_back (c);
+ if (l != nullptr)
+ args.push_back (l);
+ args.push_back (nullptr);
+ args.push_back (nullptr); // Pipe end.
+
+ try
+ {
+ out_fd = fdopen (ap,
+ fdopen_mode::out | fdopen_mode::binary |
+ fdopen_mode::truncate | fdopen_mode::create);
+ out_rm = auto_rmfile (ap);
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to open " << ap << ": " << e;
+ }
+ }
+ else
+#endif
+ {
+ if (e != "tar")
+ {
+ args.push_back ("-a");
+ if (l != nullptr)
+ args.push_back (l);
+ }
+
+ args.push_back ("-cf");
+ args.push_back (an.string ().c_str ());
+ args.push_back (base.c_str ());
+ args.push_back (nullptr);
+ }
+ }
+
+ size_t what (0); // Failed program name index in args.
+ try
+ {
+ process_path app; // Archiver path.
+ process_path cpp; // Compressor path.
+
+ app = process::path_search (args[what = 0]);
+
+ if (i != 0)
+ cpp = process::path_search (args[what = i]);
+
+ // Change the archiver's working directory to root.
+ //
+ process_env ape (app, root);
+
+ // Note: print the command line unless quiet similar to other package
+ // manager implementations.
+ //
+ if (verb >= 1)
+ print_process (ape, args);
+
+ what = 0;
+ process apr (app,
+ args.data (), // No auto-pipe.
+ 0 /* stdin */,
+ (i != 0 ? -1 : 1) /* stdout */,
+ 2 /* stderr */,
+ ape.cwd->string ().c_str (),
+ ape.vars);
+
+ // Start the compressor if required.
+ //
+ process cpr;
+ if (i != 0)
+ {
+ what = i;
+ cpr = process (cpp,
+ args.data () + i,
+ apr.in_ofd.get () /* stdin */,
+ out_fd.get () /* stdout */,
+ 2 /* stderr */);
+
+ cpr.in_ofd.reset (); // Close the archiver's stdout on our side.
+ }
+
+ // Delay throwing until we diagnose both ends of the pipe.
+ //
+ bool fail (false);
+
+ what = 0;
+ if (!apr.wait ())
+ {
+ diag_record dr (error);
+ dr << args[0] << " exited with non-zero code";
+
+ if (verb == 0)
+ {
+ info << "command line: ";
+ print_process (dr, ape, args.data ());
+ }
+
+ fail = true;
+ }
+
+ if (i != 0)
+ {
+ what = i;
+ if (!cpr.wait ())
+ {
+ diag_record dr (error);
+ dr << args[i] << " exited with non-zero code";
+
+ if (verb == 0)
+ {
+ info << "command line: ";
+ print_process (dr, args.data () + i);
+ }
+
+ fail = true;
+ }
+ }
+
+ if (fail)
+ throw failed ();
+ }
+ catch (const process_error& e)
+ {
+ error << "unable to execute " << args[what] << ": " << e;
+
+ if (e.child)
+ exit (1);
+
+ throw failed ();
+ }
+
+ out_rm.cancel ();
+ return ap;
+ }
+
+ // NOTE: THE BELOW DESCRIPTION IS ALSO REWORDED IN BPKG-PKG-BINDIST(1).
+ //
+ // The overall plan is to invoke the build system and install all the
+ // packages directly from their bpkg locations into the binary package
+ // directory as a chroot. Then tar/zip this directory to produce one or more
+ // binary package archives.
+ //
+ paths system_package_manager_archive::
+ generate (const packages& pkgs,
+ const packages& deps,
+ const strings& vars,
+ const dir_path& /*cfg_dir*/,
+ const package_manifest& pm,
+ const string& pt,
+ const small_vector<language, 1>& langs,
+ optional<recursive_mode> recur)
+ {
+ tracer trace ("system_package_manager_archive::generate");
+
+ assert (!langs.empty ()); // Should be effective.
+
+ // We require explicit output root.
+ //
+ if (!ops->output_root_specified ())
+ fail << "output root directory must be specified explicitly with "
+ << "--output-root|-o";
+
+ const dir_path& out (ops->output_root ()); // Cannot be empty.
+
+ const shared_ptr<selected_package>& sp (pkgs.front ().selected);
+ const package_name& pn (sp->name);
+ const version& pv (sp->version);
+
+ bool lib (pt == "lib");
+ bool priv (ops->private_ ()); // Private installation.
+
+ // Return true if this package uses the specified language, only as
+ // interface language if intf_only is true.
+ //
+ auto lang = [&langs] (const char* n, bool intf_only = false) -> bool
+ {
+ return find_if (langs.begin (), langs.end (),
+ [n, intf_only] (const language& l)
+ {
+ return (!intf_only || !l.impl) && l.name == n;
+ }) != langs.end ();
+ };
+
+ bool lang_c (lang ("c"));
+ bool lang_cxx (lang ("c++"));
+ bool lang_cc (lang ("cc"));
+
+ if (verb >= 3)
+ {
+ auto print_status = [] (diag_record& dr, const selected_package& p)
+ {
+ dr << (p.substate == package_substate::system ? "sys:" : "")
+ << p.name << ' ' << p.version;
+ };
+
+ {
+ diag_record dr (trace);
+ dr << "package: " ;
+ print_status (dr, *sp);
+ }
+
+ for (const package& p: deps)
+ {
+ diag_record dr (trace);
+ dr << "dependency: ";
+ print_status (dr, *p.selected);
+ }
+ }
+
+ // Should we override config.install.* or just use whatever configured
+ // (sans the root)? While using whatever configure seemed like a good idea
+ // at first, it's also a good idea to have the ability to tweak the
+ // installation directory structure on the per-platform basis (like, say,
+ // lib/libexec split or pkgconfig/ location on FreeBSD; in a sense, the
+ // user may choose to install to /usr and it would be good if things ended
+ // up in the expected places -- this is still a @@ TODO).
+ //
+ // So unless instructed otherwise with --archive-install-config, we
+ // override every config.install.* variable in order not to pick anything
+ // configured. Note that we add some more in the command line below.
+ //
+ // We make use of the <project> substitution since in the recursive mode
+ // we may be installing multiple projects. Note that the <private>
+ // directory component is automatically removed if this functionality is
+ // not enabled.
+ //
+ bool ovr_install (!ops->archive_install_config ());
+
+ strings config;
+ {
+ const string& c (target.class_);
+
+ dir_path root;
+ if (ops->archive_install_root_specified ())
+ {
+ // If specified, we override it even with --archive-install-config.
+ //
+ root = ops->archive_install_root (); // Cannot be empty.
+ }
+ else if (ovr_install)
+ {
+ if (c == "windows")
+ {
+ // Using C:\<project>\ looks like the best we can do (if the
+ // installation is not relocatable, at least related packages will
+ // be grouped together).
+ //
+ root = dir_path ("C:\\" + pm.effective_project ().string ());
+ }
+ else
+ root = dir_path ("/usr/local");
+ }
+
+ auto add = [&config] (auto&& v)
+ {
+ config.push_back (string ("config.install.") + v);
+ };
+
+ if (!root.empty ())
+ add ("root='" + root.representation () + '\'');
+
+ if (ovr_install)
+ {
+ add ("data_root=root/");
+ add ("exec_root=root/");
+
+ add ("bin=exec_root/bin/");
+ add ("sbin=exec_root/sbin/");
+
+ add ("lib=exec_root/lib/<private>/");
+ add ("libexec=exec_root/libexec/<private>/<project>/");
+ add ("pkgconfig=lib/pkgconfig/");
+
+ add ("etc=data_root/etc/");
+ add ("include=data_root/include/<private>/");
+ add ("include_arch=include/");
+ add ("share=data_root/share/");
+ add ("data=share/<private>/<project>/");
+
+ add ("doc=share/doc/<private>/<project>/");
+ add ("legal=doc/");
+ add ("man=share/man/");
+ add ("man1=man/man1/");
+ add ("man2=man/man2/");
+ add ("man3=man/man3/");
+ add ("man4=man/man4/");
+ add ("man5=man/man5/");
+ add ("man6=man/man6/");
+ add ("man7=man/man7/");
+ add ("man8=man/man8/");
+
+ add ("private=" + (priv ? pn.string () : "[null]"));
+
+ // If this is a C-based language, add rpath for private installation.
+ //
+ if (priv && (lang_c || lang_cxx || lang_cc))
+ {
+ dir_path l ((dir_path (root) /= "lib") /= pn.string ());
+ config.push_back ("config.bin.rpath='" + l.representation () + '\'');
+ }
+ }
+ }
+
+ // Add user-specified configuration variables last to allow them to
+ // override anything.
+ //
+ for (const string& v: vars)
+ config.push_back (v);
+
+ // Note that we can use weak install scope for the auto recursive mode
+ // since we know dependencies cannot be spread over multiple linked
+ // configurations.
+ //
+ string scope (!recur || *recur == recursive_mode::full
+ ? "project"
+ : "weak");
+
+ // The plan is to create the archive directory (with the same name as the
+ // archive base; we call it "destination directory") inside the output
+ // directory and then tar/zip it up placing the resulting archives next to
+ // it.
+ //
+ // Let's require clean output directory to keep things simple.
+ //
+ // Also, by default, we are going to keep all the intermediate files on
+ // failure for troubleshooting.
+ //
+ if (exists (out))
+ {
+ if (!empty (out))
+ {
+ if (!ops->wipe_output ())
+ fail << "output root directory " << out << " is not empty" <<
+ info << "use --wipe-output to clean it up but be careful";
+
+ rm_r (out, false);
+ }
+ }
+
+ // NOTE: THE BELOW DESCRIPTION IS ALSO REWORDED IN BPKG-PKG-BINDIST(1).
+ //
+ // Our archive directory/file base have the following form:
+ //
+ // <package>-<version>-<build_metadata>
+ //
+ // Where <build_metadata> in turn has the following form (unless overriden
+ // with --archive-build-mata):
+ //
+ // <cpu>-<os>[-<langrt>...]
+ //
+ // For example:
+ //
+ // hello-1.2.3-x86_64-windows10
+ // libhello-1.2.3-x86_64-windows10-msvc17.4
+ // libhello-1.2.3-x86_64-debian11-gcc12-rust1.62
+ //
+ string base (pn.string () + '-' + pv.string ());
+
+ if (ops->archive_build_meta_specified ())
+ {
+ if (!ops->archive_build_meta ().empty ())
+ base += '-' + ops->archive_build_meta ();
+ }
+ else
+ {
+ base += '-' + target.cpu;
+ base += '-' + os_release.name_id + os_release.version_id;
+
+ // First collect the interface languages and then add implementation.
+ // This way if different languages map to the same runtimes (e.g., C and
+ // C++ mapped to gcc12), then we will always prefer the interface
+ // version over the implementation (which could be different, for
+ // example, libstdc++6 vs libstdc++-12-dev; but it's not clear how this
+ // will be specified, won't they end up with different names as opposed
+ // to gcc6 and gcc12 -- still fuzzy/unclear).
+ //
+ // @@ We will need to split id and version to be able to pick the
+ // highest version.
+ //
+ // @@ Maybe we should just do "soft" version like in <distribution>?
+ //
+ // Note that we allow multiple values for the same language to support
+ // cases like --archive-lang cc=gcc12 --archive-lang cc=g++12. @@ This
+ // is TODO (need cli support for std::multimap).
+ //
+ vector<reference_wrapper<const pair<const string, string>>> langrt;
+
+ auto find = [] (const std::map<string, string>& m, const string& n)
+ {
+ auto i (m.find (n));
+
+ if (i == m.end ())
+ {
+ // If no mapping for c/c++, fallback to cc.
+ //
+ if (n == "c" || n == "c++")
+ i = m.find ("cc");
+ }
+
+ return i != m.end () ? &*i : nullptr;
+ };
+
+ auto add = [&langrt] (const pair<const string, string>& p)
+ {
+ // Suppress duplicates.
+ //
+ if (find_if (langrt.begin (), langrt.end (),
+ [&p] (const pair<const string, string>& x)
+ {
+ // @@ TODO: keep highest version.
+
+ return p.second == x.second;
+ }) == langrt.end ())
+ {
+ langrt.push_back (p);
+ }
+ };
+
+ auto& implm (ops->archive_lang_impl ());
+
+ // The interface/implementation distinction is only relevant to
+ // libraries. For everything else we treat all the languages as
+ // implementation.
+ //
+ if (lib)
+ {
+ auto& intfm (ops->archive_lang ());
+
+ for (const language& l: langs)
+ {
+ if (l.impl)
+ continue;
+
+ const pair<const string, string>* p (find (intfm, l.name));
+
+ if (p == nullptr)
+ p = find (implm, l.name);
+
+ if (p == nullptr)
+ fail << "no runtime mapping for language " << l.name <<
+ info << "consider specifying with --archive-lang[-impl]" <<
+ info << "or alternatively specify --archive-build-meta";
+
+ if (p->second.empty ())
+ continue; // Unimportant.
+
+ add (*p);
+ }
+ }
+
+ for (const language& l: langs)
+ {
+ if (lib && !l.impl)
+ continue;
+
+ const pair<const string, string>* p (find (implm, l.name));
+
+ if (p == nullptr || p->second.empty ())
+ continue; // Unimportant.
+
+ add (*p);
+ }
+
+ for (const pair<const string, string>& p: langrt)
+ base += '-' + p.second;
+ }
+
+ dir_path dst (out / dir_path (base));
+ mk_p (dst);
+
+ // Update and install.
+ //
+ // In a sense, this is a special version of pkg-install.
+ //
+ {
+ strings dirs;
+ for (const package& p: pkgs)
+ dirs.push_back (p.out_root.representation ());
+
+ run_b (*ops,
+ verb_b::normal,
+ (ops->jobs_specified ()
+ ? strings ({"--jobs", to_string (ops->jobs ())})
+ : strings ()),
+ "config.install.chroot='" + dst.representation () + '\'',
+ (ovr_install ? "config.install.sudo=[null]" : nullptr),
+ config,
+ "!config.install.scope=" + scope,
+ "install:",
+ dirs);
+
+ // @@ TODO: call install.json? Or manifest-install.json. Place in data/
+ // (would need support in build2 to use install.* values)?
+ //
+#if 0
+ args.push_back ("!config.install.manifest=-");
+#endif
+ }
+
+ // @@ TODO: metadata manifest.
+ //
+ // @@ TODO: add homepage, maintainer to manifest?
+ // @@ TODO: also add dependencies?
+ // @@ TODO: also add timestamp, priority like in Debian?
+ // @@ TODO: also add licenses like in Debian?
+ // @@ TODO: include languages in manifest (both intf and impl)?
+ //
+#if 0
+ string homepage (pm.package_url ? pm.package_url->string () :
+ pm.url ? pm.url->string () :
+ string ());
+
+ string maintainer;
+ if ( const email* e = (pm.package_email ? &*pm.package_email :
+ pm.email ? &*pm.email :
+ nullptr))
+ {
+ // In certain places (e.g., changelog), Debian expect this to be in the
+ // `John Doe <john@example.org>` form while we often specify just the
+ // email address (e.g., to the mailing list). Try to detect such a case
+ // and complete it to the desired format.
+ //
+ if (e->find (' ') == string::npos && e->find ('@') != string::npos)
+ {
+ // Try to use comment as name, if any.
+ //
+ if (!e->comment.empty ())
+ maintainer = e->comment;
+ else
+ maintainer = pn.string () + " package maintainer";
+
+ maintainer += " <" + *e + '>';
+ }
+ else
+ maintainer = *e;
+ }
+#endif
+
+ if (ops->archive_prepare_only ())
+ {
+ if (verb >= 1)
+ text << "prepared " << dst;
+
+ return paths {};
+ }
+
+ // Create the archive.
+ //
+ // Should the default archive type be based on host or target? I guess
+ // that depends on where the result will be unpacked, and it feels like
+ // target is more likely.
+ //
+ // @@ What about the ownerhip of the resulting file in the archive?
+ // We don't do anything for source archives, not sure why we should
+ // do something here.
+ //
+ paths r;
+ {
+ const strings& ts (
+ ops->archive_type_specified ()
+ ? ops->archive_type ()
+ : strings {target.class_ == "windows" ? "zip" : "tar.xz"});
+
+ for (string t: ts)
+ {
+ // Help the user out if the extension is specified with the leading
+ // dot.
+ //
+ if (t.size () > 1 && t.front () == '.')
+ t.erase (0, 1);
+
+ r.push_back (archive (out, base, t));
+ }
+ }
+
+ // Cleanup intermediate files unless requested not to.
+ //
+ if (!ops->keep_output ())
+ {
+ rm_r (dst);
+ }
+
+ return r;
+ }
+
+ optional<const system_package_status*> system_package_manager_archive::
+ pkg_status (const package_name&, const available_packages*)
+ {
+ assert (false);
+ return nullopt;
+ }
+
+ void system_package_manager_archive::
+ pkg_install (const vector<package_name>&)
+ {
+ assert (false);
+ }
+}
diff --git a/bpkg/system-package-manager-archive.hxx b/bpkg/system-package-manager-archive.hxx
new file mode 100644
index 0000000..0d27d85
--- /dev/null
+++ b/bpkg/system-package-manager-archive.hxx
@@ -0,0 +1,54 @@
+// file : bpkg/system-package-manager-archive.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BPKG_SYSTEM_PACKAGE_MANAGER_ARCHIVE_HXX
+#define BPKG_SYSTEM_PACKAGE_MANAGER_ARCHIVE_HXX
+
+#include <bpkg/types.hxx>
+#include <bpkg/utility.hxx>
+
+#include <bpkg/system-package-manager.hxx>
+
+namespace bpkg
+{
+ // The system package manager implementation for the installation archive
+ // packages, production only.
+ //
+ class system_package_manager_archive: public system_package_manager
+ {
+ public:
+ virtual paths
+ generate (const packages&,
+ const packages&,
+ const strings&,
+ const dir_path&,
+ const package_manifest&,
+ const string&,
+ const small_vector<language, 1>&,
+ optional<recursive_mode>) override;
+
+ virtual optional<const system_package_status*>
+ pkg_status (const package_name&, const available_packages*) override;
+
+ virtual void
+ pkg_install (const vector<package_name>&) override;
+
+ public:
+ // Note: options can only be NULL when testing functions that don't need
+ // them.
+ //
+ system_package_manager_archive (bpkg::os_release&&,
+ const target_triplet& host,
+ string arch,
+ optional<bool> progress,
+ const pkg_bindist_options*);
+
+ protected:
+ // Only for production.
+ //
+ const pkg_bindist_options* ops = nullptr;
+ target_triplet target;
+ };
+}
+
+#endif // BPKG_SYSTEM_PACKAGE_MANAGER_ARCHIVE_HXX
diff --git a/bpkg/system-package-manager-debian.cxx b/bpkg/system-package-manager-debian.cxx
index 7be5be5..9370f37 100644
--- a/bpkg/system-package-manager-debian.cxx
+++ b/bpkg/system-package-manager-debian.cxx
@@ -765,12 +765,7 @@ namespace bpkg
}
catch (const process_error& e)
{
- error << "unable to execute " << args[0] << ": " << e;
-
- if (e.child)
- exit (1);
-
- throw failed ();
+ fail << "unable to execute " << args[0] << ": " << e << endf;
}
}
diff --git a/bpkg/system-package-manager-fedora.cxx b/bpkg/system-package-manager-fedora.cxx
index 576e0ef..cfaa636 100644
--- a/bpkg/system-package-manager-fedora.cxx
+++ b/bpkg/system-package-manager-fedora.cxx
@@ -818,12 +818,7 @@ namespace bpkg
}
catch (const process_error& e)
{
- error << "unable to execute " << args[0] << ": " << e;
-
- if (e.child)
- exit (1);
-
- throw failed ();
+ fail << "unable to execute " << args[0] << ": " << e << endf;
}
}
diff --git a/bpkg/system-package-manager.cxx b/bpkg/system-package-manager.cxx
index 9e54418..c4ebe5a 100644
--- a/bpkg/system-package-manager.cxx
+++ b/bpkg/system-package-manager.cxx
@@ -18,6 +18,7 @@
#include <bpkg/system-package-manager-debian.hxx>
#include <bpkg/system-package-manager-fedora.hxx>
+#include <bpkg/system-package-manager-archive.hxx>
using namespace std;
using namespace butl;
@@ -147,13 +148,35 @@ namespace bpkg
o.no_progress () ? false :
optional<bool> ());
- unique_ptr<system_package_manager> r;
+ optional<os_release> oos;
+ if (o.os_release_id_specified ())
+ {
+ oos = os_release ();
+ oos->name_id = o.os_release_id ();
+ }
+ else
+ oos = host_release (host);
- if (optional<os_release> oos = host_release (host))
+ if (o.os_release_name_specified ())
+ oos->name = o.os_release_name ();
+
+ if (o.os_release_version_id_specified ())
+ oos->version_id = o.os_release_version_id ();
+
+ unique_ptr<system_package_manager> r;
+ if (oos)
{
os_release& os (*oos);
- if (host.class_ == "linux")
+ // Note that we don't make archive the default on any platform in case
+ // we later want to support its native package format.
+ //
+ if (name == "archive")
+ {
+ r.reset (new system_package_manager_archive (
+ move (os), host, arch, progress, &o));
+ }
+ else if (host.class_ == "linux")
{
if (is_or_like (os, "debian") ||
is_or_like (os, "ubuntu"))
diff --git a/bpkg/system-package-manager.hxx b/bpkg/system-package-manager.hxx
index 4ae2e07..6cdb2d4 100644
--- a/bpkg/system-package-manager.hxx
+++ b/bpkg/system-package-manager.hxx
@@ -411,8 +411,9 @@ namespace bpkg
// this platform. If architecture is empty, then derive it automatically
// from the host target triplet. Currently recognized names:
//
- // debian -- Debian and alike (Ubuntu, etc) using the APT frontend.
- // fedora -- Fedora and alike (RHEL, Centos, etc) using the DNF frontend.
+ // debian -- Debian and alike (Ubuntu, etc) using the APT frontend.
+ // fedora -- Fedora and alike (RHEL, Centos, etc) using the DNF frontend.
+ // archive -- Installation archive, any platform, production only.
//
// Note: the name can be used to select an alternative package manager
// implementation on platforms that support multiple.
diff --git a/doc/cli.sh b/doc/cli.sh
index 4f7ea18..6e50bed 100755
--- a/doc/cli.sh
+++ b/doc/cli.sh
@@ -78,7 +78,9 @@ compile "bpkg" $o --output-prefix "" --class-doc bpkg::commands=short --class-do
compile "pkg-build" $o --class-doc bpkg::pkg_build_pkg_options=exclude-base
-compile "pkg-bindist" $o --class-doc bpkg::pkg_bindist_debian_options=exclude-base
+compile "pkg-bindist" $o \
+ --class-doc bpkg::pkg_bindist_debian_options=exclude-base \
+ --class-doc bpkg::pkg_bindist_archive_options=exclude-base
# NOTE: remember to update a similar list in buildfile and bpkg.cli as well as
# the help topics sections in bpkg/buildfile and help.cxx.