From afd0b8699b009b96be34ba2a20441ecb223957ce Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 14 Mar 2023 09:32:22 +0200 Subject: Add support for generating installation archives in pkg-bindist --- bpkg/buildfile | 11 +- bpkg/pkg-bindist.cli | 220 +++++++++- bpkg/pkg-bindist.cxx | 14 +- bpkg/system-package-manager-archive.cxx | 744 ++++++++++++++++++++++++++++++++ bpkg/system-package-manager-archive.hxx | 54 +++ bpkg/system-package-manager-debian.cxx | 7 +- bpkg/system-package-manager-fedora.cxx | 7 +- bpkg/system-package-manager.cxx | 29 +- bpkg/system-package-manager.hxx | 5 +- doc/cli.sh | 4 +- 10 files changed, 1063 insertions(+), 32 deletions(-) create mode 100644 bpkg/system-package-manager-archive.cxx create mode 100644 bpkg/system-package-manager-archive.hxx 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 ; + include ; "\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 + { + "", + "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 --archive-lang + { + "=", + "Map interface language name to runtime id . 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 --archive-lang-impl + { + "=", + "Map implementation language name to runtime id . 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 + { + "", + "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 + { + "", + "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 { "", "Alternative system/distribution package manager to generate the binary - package for. The valid 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 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 + { + "", + "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 + { + "", + "Override the \cb{VERSION_ID} component in \cb{os-release(5)} or + equivalent." + } + + string --os-release-name + { + "", + "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 + +#include + +#include + +using namespace butl; + +namespace bpkg +{ + system_package_manager_archive:: + system_package_manager_archive (bpkg::os_release&& osr, + const target_triplet& h, + string a, + optional 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= tar|zip ... . + // + // 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& langs, + optional 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& 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 substitution since in the recursive mode + // we may be installing multiple projects. Note that the + // 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:\\ 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//"); + add ("libexec=exec_root/libexec///"); + add ("pkgconfig=lib/pkgconfig/"); + + add ("etc=data_root/etc/"); + add ("include=data_root/include//"); + add ("include_arch=include/"); + add ("share=data_root/share/"); + add ("data=share///"); + + add ("doc=share/doc///"); + 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: + // + // -- + // + // Where in turn has the following form (unless overriden + // with --archive-build-mata): + // + // -[-...] + // + // 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 ? + // + // 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>> langrt; + + auto find = [] (const std::map& 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& p) + { + // Suppress duplicates. + // + if (find_if (langrt.begin (), langrt.end (), + [&p] (const pair& 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* 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* p (find (implm, l.name)); + + if (p == nullptr || p->second.empty ()) + continue; // Unimportant. + + add (*p); + } + + for (const pair& 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 ` 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 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&) + { + 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 +#include + +#include + +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&, + optional) override; + + virtual optional + pkg_status (const package_name&, const available_packages*) override; + + virtual void + pkg_install (const vector&) 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 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 #include +#include using namespace std; using namespace butl; @@ -147,13 +148,35 @@ namespace bpkg o.no_progress () ? false : optional ()); - unique_ptr r; + optional oos; + if (o.os_release_id_specified ()) + { + oos = os_release (); + oos->name_id = o.os_release_id (); + } + else + oos = host_release (host); - if (optional 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 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. -- cgit v1.1