From 854c668b5e63e26a9d7a6e55226a0940638e0453 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 9 Feb 2023 15:46:32 +0200 Subject: Add pkg-bindist command (generate binary distribution package) This commit includes an implementation for Debian and alike. --- bpkg/system-package-manager-debian.cxx | 1928 +++++++++++++++++++++++++++++++- 1 file changed, 1867 insertions(+), 61 deletions(-) (limited to 'bpkg/system-package-manager-debian.cxx') diff --git a/bpkg/system-package-manager-debian.cxx b/bpkg/system-package-manager-debian.cxx index b541541..747d037 100644 --- a/bpkg/system-package-manager-debian.cxx +++ b/bpkg/system-package-manager-debian.cxx @@ -3,8 +3,15 @@ #include +#include + +#include +#include // permissions + #include +#include + using namespace butl; namespace bpkg @@ -24,7 +31,8 @@ namespace bpkg c; } - // Parse the debian-name (or alike) value. + // Parse the debian-name (or alike) value. The first argument is the package + // type. // // Note that for now we treat all the packages from the non-main groups as // extras omitting the -common package (assuming it's pulled by the main @@ -32,7 +40,7 @@ namespace bpkg // extra_{doc,dbg} arguments. // package_status system_package_manager_debian:: - parse_name_value (const package_name& pn, + parse_name_value (const string& pt, const string& nv, bool extra_doc, bool extra_dbg) @@ -52,8 +60,7 @@ namespace bpkg return nn > sn && n.compare (nn - sn, sn, s) == 0; }; - auto parse_group = [&split, &suffix] (const string& g, - const package_name* pn) + auto parse_group = [&split, &suffix] (const string& g, const string* pt) { strings ns (split (g, ' ')); @@ -64,8 +71,6 @@ namespace bpkg // Handle the "dev instead of main" special case for libraries. // - // Note: the lib prefix check is based on the bpkg package name. - // // Check that the following name does not end with -dev. This will be // the only way to disambiguate the case where the library name happens // to end with -dev (e.g., libfoo-dev libfoo-dev-dev). @@ -73,10 +78,9 @@ namespace bpkg { string& m (ns[0]); - if (pn != nullptr && - pn->string ().compare (0, 3, "lib") == 0 && - pn->string ().size () > 3 && - suffix (m, "-dev") && + if (pt != nullptr && + *pt == "lib" && + suffix (m, "-dev") && !(ns.size () > 1 && suffix (ns[1], "-dev"))) { r = package_status ("", move (m)); @@ -117,7 +121,7 @@ namespace bpkg for (size_t i (0); i != gs.size (); ++i) { if (i == 0) // Main group. - r = parse_group (gs[i], &pn); + r = parse_group (gs[i], &pt); else { package_status g (parse_group (gs[i], nullptr)); @@ -894,14 +898,6 @@ namespace bpkg optional system_package_manager_debian:: pkg_status (const package_name& pn, const available_packages* aps) { - // For now we ignore -doc and -dbg package components (but we may want to - // have options controlling this later). Note also that we assume -common - // is pulled automatically by the main package so we ignore it as well - // (see equivalent logic in parse_name_value()). - // - bool need_doc (false); - bool need_dbg (false); - // First check the cache. // { @@ -914,6 +910,25 @@ namespace bpkg return nullopt; } + optional r (status (pn, *aps)); + + // Cache. + // + auto i (status_cache_.emplace (pn, move (r)).first); + return i->second ? &*i->second : nullptr; + } + + optional system_package_manager_debian:: + status (const package_name& pn, const available_packages& aps) + { + // For now we ignore -doc and -dbg package components (but we may want to + // have options controlling this later). Note also that we assume -common + // is pulled automatically by the main package so we ignore it as well + // (see equivalent logic in parse_name_value()). + // + bool need_doc (false); + bool need_dbg (false); + vector candidates; // Translate our package name to the Debian package names. @@ -926,12 +941,26 @@ namespace bpkg << os_release.name_id << " package name"; }); + // Without explicit type, the best we can do in trying to detect whether + // this is a library is to check for the lib prefix. Libraries without + // the lib prefix and non-libraries with the lib prefix (both of which + // we do not recomment) will have to provide a manual mapping (or + // explicit type). + // + // Note that using the first (latest) available package as a source of + // type information seems like a reasonable choice. + // + const string& pt (!aps.empty () + ? aps.front ().first->effective_type () + : package_manifest::effective_type (nullopt, pn)); + strings ns; - if (!aps->empty ()) - ns = system_package_names (*aps, + if (!aps.empty ()) + ns = system_package_names (aps, os_release.name_id, os_release.version_id, - os_release.like_ids); + os_release.like_ids, + true /* native */); if (ns.empty ()) { // Attempt to automatically translate our package name (see above for @@ -939,12 +968,7 @@ namespace bpkg // const string& n (pn.string ()); - // The best we can do in trying to detect whether this is a library is - // to check for the lib prefix. Libraries without the lib prefix and - // non-libraries with the lib prefix (both of which we do not - // recomment) will have to provide a manual mapping. - // - if (n.compare (0, 3, "lib") == 0 && n.size () > 3) + if (pt == "lib") { // Keep the main package name empty as an indication that it is to // be discovered. @@ -960,7 +984,7 @@ namespace bpkg // for (const string& n: ns) { - package_status s (parse_name_value (pn, n, need_doc, need_dbg)); + package_status s (parse_name_value (pt, n, need_doc, need_dbg)); // Suppress duplicates for good measure based on the main package // name (and falling back to -dev if empty). @@ -1269,9 +1293,9 @@ namespace bpkg string sv (r->system_version, 0, r->system_version.rfind ('-')); optional v; - if (!aps->empty ()) + if (!aps.empty ()) v = downstream_package_version (sv, - *aps, + aps, os_release.name_id, os_release.version_id, os_release.like_ids); @@ -1304,10 +1328,7 @@ namespace bpkg r->version = move (*v); } - // Cache. - // - auto i (status_cache_.emplace (pn, move (r)).first); - return i->second ? &*i->second : nullptr; + return r; } void system_package_manager_debian:: @@ -1447,6 +1468,339 @@ namespace bpkg } } + // Map non-system bpkg package to system package name(s) and version. + // + // This is used both to map the package being generated and its + // dependencies. What should we do with extras returned in package_status? + // We can't really generate any of them (which files would we place in + // them?) nor can we list them as dependencies (we don't know their system + // versions). So it feels like the only sensible choice is to ignore extras. + // + // In a sense, we have a parallel arrangement going on here: binary packages + // that we generate don't have extras (i.e., they include everything + // necessary in the "standard" packages from the main group) and when we + // punch a system dependency based on a non-system bpkg package, we assume + // it was generated by us and thus doesn't have any extras. Or, to put it + // another way, if you want the system dependency to refer to a "native" + // system package with extras you need to configure it as a system bpkg + // package. + // + // In fact, this extends to package names. For example, unless custom + // mapping is specified, we will generate libsqlite3 and libsqlite3-dev + // while native names are libsqlite3-0 and libsqlite3-dev. While this + // duality is not ideal, presumably we will normally only be producing our + // binary packages if there are no suitable native packages. And for a few + // exception (e.g., our package is "better" in some way, such as configured + // differently or fixes a critical bug), we will just have to provide + // appropriate manual mapping that makes sure the names match (the extras is + // still a potential problem though -- we will only have them as + // dependencies if we build against a native system package; maybe we can + // add them manually with an option). + // + package_status system_package_manager_debian:: + map_package (const package_name& pn, + const version& pv, + const available_packages& aps, + const optional& build_metadata) const + { + // We should only have one available package corresponding to this package + // name/version. + // + assert (aps.size () == 1); + + const shared_ptr& ap (aps.front ().first); + const lazy_shared_ptr& rf (aps.front ().second); + + // Without explicit type, the best we can do in trying to detect whether + // this is a library is to check for the lib prefix. Libraries without the + // lib prefix and non-libraries with the lib prefix (both of which we do + // not recomment) will have to provide a manual mapping (or explicit + // type). + // + const string& pt (ap->effective_type ()); + + strings ns (system_package_names (aps, + os_release.name_id, + os_release.version_id, + os_release.like_ids, + false /* native */)); + package_status r; + if (ns.empty ()) + { + // Automatically translate our package name similar to the consumption + // case above. Except here we don't attempt to deduce main from -dev, + // naturally. + // + const string& n (pn.string ()); + + if (pt == "lib") + r = package_status (n, n + "-dev"); + else + r = package_status (n); + } + else + { + // Even though we only pass one available package, we may still end up + // with multiple mappings. In this case we take the first, per the + // documentation. + // + r = parse_name_value (pt, + ns.front (), + false /* need_doc */, + false /* need_dbg */); + + // If this is -dev without main, then derive main by stripping the -dev + // suffix. This feels tighter than just using the bpkg package name. + // + if (r.main.empty ()) + { + assert (!r.dev.empty ()); + r.main.assign (r.dev, 0, r.dev.size () - 4); + } + } + + // Map the version. + // + // NOTE: THE BELOW DESCRIPTION IS ALSO REPRODUCED IN THE BPKG MANUAL. + // + // To recap, a Debian package version has the following form: + // + // [:][-] + // + // For details on the ordering semantics, see the Version control file + // field documentation in the Debian Policy Manual. While overall + // unsurprising, one notable exception is `~`, which sorts before anything + // else and is commonly used for upstream pre-releases. For example, + // 1.0~beta1~svn1245 sorts earlier than 1.0~beta1, which sorts earlier + // than 1.0. + // + // There are also various special version conventions (such as all the + // revision components in 1.4-5+deb10u1~bpo9u1) but they all appear to + // express relationships between native packages and/or their upstream and + // thus do not apply to our case. + // + // Ok, so how do we map our version to that? To recap, the bpkg version + // has the following form: + // + // [+-][-][+] + // + // Let's start with the case where neither distribution nor upstream + // version is specified and we need to derive everything from the bpkg + // version. + // + // + // + // On one hand, if we keep the epoch, it won't necessarily match + // Debian's native package epoch. But on the other it will allow our + // binary packages from different epochs to co-exist. Seeing that this + // can be easily overridden with a custom distribution version, let's + // keep it. + // + // Note that while the Debian start/default epoch is 0, ours is 1 (we + // use the 0 epoch for stub packages). So we will need to shift this + // value range. + // + // + // [-] + // + // Our upstream version maps naturally to Debian's. That is, our + // upstream version format/semantics is a subset of Debian's. + // + // If this is a pre-release, then we could fail (that is, don't allow + // pre-releases) but then we won't be able to test on pre-release + // packages, for example, to make sure the name mapping is correct. + // Plus sometimes it's useful to publish pre-releases. We could ignore + // it, but then such packages will be indistinguishable from each other + // and the final release, which is not ideal. On the other hand, Debian + // has the mechanism (`~`) which is essentially meant for this, so let's + // use it. We will use as is since its format is the same as + // upstream and thus should map naturally. + // + // + // + // + // Similar to epoch, our revision won't necessarily match Debian's + // native package revision. But on the other hand it will allow us to + // establish a correspondence between source and binary packages. Plus, + // upgrades between binary package revisions will be handled naturally. + // Seeing that we allow overriding the revision with a custom + // distribution version (see below), let's keep it. + // + // Note also that both Debian and our revision start/default is 0. + // However, it is Debian's convention to start revision from 1. But it + // doesn't seem worth it for us to do any shifting here and so we will + // use our revision as is. + // + // Another related question is whether we should also include some + // metadata that identifies the distribution and its version that this + // package is for. The strongest precedent here is probably Ubuntu's + // PPA. While there doesn't appear to be a consistent approach, one can + // often see versions like these: + // + // 2.1.0-1~ppa0~ubuntu14.04.1, + // 1.4-5-1.2.1~ubuntu20.04.1~ppa1 + // 22.12.2-0ubuntu1~ubuntu23.04~ppa1 + // + // Seeing that this is a non-sortable component (what in semver would be + // called "build metadata"), using `~` is probably not the worst choice. + // + // So we follow this lead and add the ~ component + // to revision. Note that this also means we will have to make the 0 + // revision explicit. For example: + // + // 1.2.3-1~debian10 + // 1.2.3-0~ubuntu20.04 + // + // The next case to consider is when we have the upstream version + // (upstream-version manifest value). After some rumination it feels + // correct to use it in place of the - components in the + // above mapping (upstream version itself cannot have epoch). In other + // words, we will add the pre-release and revision components from the + // bpkg version. If this is not the desired semantics, then it can always + // be overrided with the distribution version. + // + // Finally, we have the distribution version. The Debian and + // components are straightforward: they should be specified by + // the distribution version as required. This leaves pre-release and + // revision. It feels like in most cases we would want these copied over + // from the bpkg version automatically -- it's too tedious and error- + // prone to maintain them manually. However, we want the user to have the + // full override ability. So instead, if empty revision is specified, as + // in 1.2.3-, then we automatically add the bpkg revision. Similarly, if + // empty pre-release is specified, as in 1.2.3~, then we add the bpkg + // pre-release. To add both automatically, we would specify 1.2.3~- (other + // combinations are 1.2.3~b.1- and 1.2.3~-1). + // + // Note also that per the Debian version specification, if upstream + // contains `:` and/or `-`, then epoch and/or revision must be specified + // explicitly, respectively. Note that the bpkg upstream version may not + // contain either. + // + string& sv (r.system_version); + + bool no_build_metadata (build_metadata && build_metadata->empty ()); + + if (optional ov = system_package_version (ap, + rf, + os_release.name_id, + os_release.version_id, + os_release.like_ids)) + { + string& dv (*ov); + size_t n (dv.size ()); + + // Find the revision and pre-release positions, if any. + // + size_t rp (dv.rfind ('-')); + size_t pp (dv.rfind ('~', rp)); + + // Copy over the [:] part. + // + sv.assign (dv, 0, pp < rp ? pp : rp); + + // Add pre-release copying over the bpkg version value if empty. + // + if (pp != string::npos) + { + if (size_t pn = (rp != string::npos ? rp : n) - (pp + 1)) + { + sv.append (dv, pp, pn + 1); + } + else + { + if (pv.release) + { + assert (!pv.release->empty ()); // Cannot be earliest special. + sv += '~'; + sv += *pv.release; + } + } + } + + // Add revision copying over the bpkg version value if empty. + // + // Omit the default -0 revision if we have no build metadata. + // + if (rp != string::npos) + { + if (size_t rn = n - (rp + 1)) + { + sv.append (dv, rp, rn + 1); + } + else if (pv.revision || !no_build_metadata) + { + sv += '-'; + sv += to_string (pv.revision ? *pv.revision : 0); + } + } + else if (!no_build_metadata) + sv += "-0"; // Default revision (for build metadata; see below). + } + else + { + if (ap->upstream_version) + { + const string& uv (*ap->upstream_version); + + // Add explicit epoch if upstream contains `:`. + // + // Note that we don't need to worry about `-` since we always add + // revision (see below). + // + if (uv.find (':') != string::npos) + sv = "0:"; + + sv += uv; + } + else + { + // Add epoch unless maps to 0. + // + assert (pv.epoch != 0); // Cannot be a stub. + if (pv.epoch != 1) + { + sv = to_string (pv.epoch - 1); + sv += ':'; + } + + sv += pv.upstream; + } + + // Add pre-release. + // + if (pv.release) + { + assert (!pv.release->empty ()); // Cannot be earliest special. + sv += '~'; + sv += *pv.release; + } + + // Add revision. + // + if (pv.revision || !no_build_metadata) + { + sv += '-'; + sv += to_string (pv.revision ? *pv.revision : 0); + } + } + + // Add build matadata. + // + if (!no_build_metadata) + { + sv += '~'; + if (build_metadata) + sv += *build_metadata; + else + { + sv += os_release.name_id; + sv += os_release.version_id; // Could be empty. + } + } + + return r; + } + // Some background on creating Debian packages (for a bit more detailed // overview see the Debian Packaging Tutorial). // @@ -1455,8 +1809,8 @@ namespace bpkg // create the package completely manually without using any of the Debian // tools and while some implementations (for example, cargo-deb) do it this // way, we are not going to go this route because it does not scale well to - // more complex packages which may require additional functionality, such as - // managing systemd files, and which is covered by the Debian tools (for an + // more complex packages which may require additional functionality (such as + // managing systemd files) and which is covered by the Debian tools (for an // example of where this leads, see the partial debhelper re-implementation // in cargo-deb). Another issues with this approach is that it's not // amenable to customizations, at least not in a way familiar to Debian @@ -1464,24 +1818,24 @@ namespace bpkg // // At the lowest level of the Debian tools for creating packages sits the // dpkg-deb --build|-b command (also accessible as dpkg --build|-b). Given a - // directory with all the binary contents (including the package metadata, - // such as the control file, in the debian/ subdirectory) this command will - // pack everything up into a .deb file. While an improvement over the fully - // manual packaging, this approach has essentially the same drawbacks. In - // particular, this command generates a single package which means we will - // have to manually sort out things into -dev, -doc, etc. + // directory with all the binary package contents (including the package + // metadata, such as the control file, in the debian/ subdirectory) this + // command will pack everything up into a .deb file. While an improvement + // over the fully manual packaging, this approach has essentially the same + // drawbacks. In particular, this command generates a single package which + // means we will have to manually sort out things into -dev, -doc, etc. // // Next up the stack is dpkg-buildpackage. This tool expects the package to - // follow the Debian way, that is, to provide the debian/rules makefile with - // a number of required targets which it then invokes to build, install, and - // pack a package from source (and somewhere in this process it calls - // dpkg-deb --build). The dpkg-buildpackage(1) man page has an overview of - // all the steps that this command performs and it is the recommended, - // lower-level, way to build packages on Debian. + // follow the Debian way of packaging, that is, to provide the debian/rules + // makefile with a number of required targets which it then invokes to + // build, install, and pack a package from source (and sometime during this + // process it calls dpkg-deb --build). The dpkg-buildpackage(1) man page has + // an overview of all the steps that this command performs and it is the + // recommended, lower-level, way to build packages on Debian. // // At the top of the stack sits debuild which calls dpkg-buildpackage, then - // lintian and finally design (though signing can also be performed by - // dpkg-buildpackage). + // lintian, and finally design (though signing can also be performed by + // dpkg-buildpackage itself). // // Based on this our plan is to use dpkg-buildpackage which brings us to the // Debian way of packaging with debian/rules at its core. As it turns out, @@ -1498,26 +1852,1478 @@ namespace bpkg // While debhelper tools definitely simplify debian/rules, there is often // still a lot of boilerplate code. So second-level helpers are often used, // with the dominant option being the dh(1) command sequencer (there is also - // CDBS but it appears to be mostly obsolete). + // CDBS but it appears to be fading into obsolescence). // // Based on that our options appear to be classic debhelper and dh. Looking // at the statistics, it's clear that the majority of packages (including // fairly complex ones) tend to prefer dh and there is no reason for us to // try to buck this trend. // + // NOTE: THE BELOW DESCRIPTION IS ALSO REWORDED IN BPKG-PKG-BINDIST(1). + // // So, to sum up, the plan is to produce debian/rules that uses the dh // command sequencer and then invoke dpkg-buildpackage to produce the binary // package from that. While this approach is normally used to build things - // from source, it feels like we should be able to pretend that we are by, - // for example, overriding the install target to invoke the build system to - // install all the packages directly from their bpkg locations. + // from source, it feels like we should be able to pretend that we are. + // Specifially, we can override the install target to invoke the build + // system and install all the packages directly from their bpkg locations. // - void system_package_manager_debian:: - generate (packages&&, - packages&&, - strings&&, - const dir_path&, - optional) + // Note that the -dbgsym packages are generated by default and all we need + // to do from our side is to compile with debug information (-g), failed + // which we get a warning from debhelper. + // + // Note: this setup requires dpkg-dev (or build-essential) and debhelper + // packages. + // + paths system_package_manager_debian:: + 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_debian::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. + + optional build_metadata; + if (ops_->debian_build_meta_specified ()) + build_metadata = ops_->debian_build_meta (); + + const shared_ptr& sp (pkgs.front ().selected); + const package_name& pn (sp->name); + const version& pv (sp->version); + + const available_packages& aps (pkgs.front ().available); + + bool lib (pt == "lib"); + bool priv (ops_->private_ ()); // Private installation. + + // For now we only know how to handle libraries with C-common interface + // languages. But we allow other implementation languages. + // + if (lib) + { + for (const language& l: langs) + if (!l.impl && l.name != "c" && l.name != "c++" && l.name != "cc") + fail << l.name << " libraries are not yet supported"; + } + + // 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 (); + }; + + // As a first step, figure out the system names and version of the package + // we are generating and all the dependencies, diagnosing anything fishy. + // + // Note that there should be no duplicate dependencies and we can sidestep + // the status cache. + // + package_status st (map_package (pn, pv, aps, build_metadata)); + + vector sdeps; + sdeps.reserve (deps.size ()); + for (const package& p: deps) + { + const shared_ptr& sp (p.selected); + const available_packages& aps (p.available); + + package_status s; + if (sp->substate == package_substate::system) + { + optional os (status (sp->name, aps)); + + if (!os || os->status != package_status::installed) + fail << os_release.name_id << " package for " << sp->name + << " system package is no longer installed"; + + // For good measure verify the mapped back version still matches + // configured. Note that besides the normal case (queried by the + // system package manager), it could have also been specified by the + // user as an actual version or a wildcard. Ignoring this check for a + // wildcard feels consistent with the overall semantics. + // + if (sp->version != wildcard_version && sp->version != os->version) + { + fail << "current " << os_release.name_id << " package version for " + << sp->name << " system package does not match configured" << + info << "configured version: " << sp->version << + info << "current version: " << os->version << " (" + << os->system_version << ')'; + } + + s = move (*os); + } + else + s = map_package (sp->name, sp->version, aps, build_metadata); + + sdeps.push_back (move (s)); + } + + if (verb >= 3) + { + auto print_status = [] (diag_record& dr, const package_status& s) + { + dr << s.main + << (s.dev.empty () ? "" : " ") << s.dev + << (s.doc.empty () ? "" : " ") << s.doc + << (s.dbg.empty () ? "" : " ") << s.dbg + << (s.common.empty () ? "" : " ") << s.common + << ' ' << s.system_version; + }; + + { + diag_record dr (trace); + dr << "package: "; + print_status (dr, st); + } + + for (const package_status& st: sdeps) + { + diag_record dr (trace); + dr << "dependency: "; + print_status (dr, st); + } + } + + if (!st.dbg.empty ()) + fail << "generation of obsolete manual -dbg packages not supported" << + info << "use automatic -dbgsym packages instead"; + + // We override every config.install.* variable in order not to pick + // anything configured. Note that we add some more in the rules file + // 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. One side-effect of using is that we will be + // using the bpkg package name instead of the main Debian package name. + // But perhaps that's correct: on Debian it's usually the source package + // name, which is the same. To keep things consistent we use the bpkg + // package name for as well. + // + // Note that some libraries have what looks like architecture-specific + // configuration files in /usr/include/$(DEB_HOST_MULTIARCH)/ which is + // what we use for our config.install.include_arch location. + // + // Note: we need to quote values that contain `$` so that they don't get + // expanded as build2 variables in the installed_entries() call. + // + // NOTE: make sure to update .install files below if changing anyting + // here. + // + strings config { + "config.install.root=/usr/", + "config.install.data_root=root/", + "config.install.exec_root=root/", + + "config.install.bin=exec_root/bin/", + "config.install.sbin=exec_root/sbin/", + + // On Debian shared libraries should not be executable. Also, + // libexec/ is the same as lib/ (note that executables that get + // installed there will still have the executable bit set). + // + "config.install.lib='exec_root/lib/$(DEB_HOST_MULTIARCH)//'", + "config.install.lib.mode=644", + "config.install.libexec=lib//", + "config.install.pkgconfig=lib/pkgconfig/", + + "config.install.etc=/etc/", + "config.install.include=data_root/include//", + "config.install.include_arch='data_root/include/$(DEB_HOST_MULTIARCH)//'", + "config.install.share=data_root/share/", + "config.install.data=share///", + + "config.install.doc=share/doc///", + "config.install.legal=doc/", + "config.install.man=share/man/", + "config.install.man1=man/man1/", + "config.install.man2=man/man2/", + "config.install.man3=man/man3/", + "config.install.man4=man/man4/", + "config.install.man5=man/man5/", + "config.install.man6=man/man6/", + "config.install.man7=man/man7/", + "config.install.man8=man/man8/"}; + + config.push_back ("config.install.private=" + + (priv ? pn.string () : "[null]")); + + // 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"); + + // Get the map of files that will end up in the binary packages. + // + // Note that we are passing quoted values with $(DEB_HOST_MULTIARCH) which + // will be treated literally. + // + installed_entry_map ies (installed_entries (*ops_, pkgs, config, scope)); + + if (ies.empty ()) + fail << "specified package(s) do not install any files"; + + if (verb >= 4) + { + for (const auto& p: ies) + { + diag_record dr (trace); + dr << "installed entry: " << p.first; + + if (p.second.target != nullptr) + dr << " -> " << p.second.target->first; // Symlink. + else + dr << " " << p.second.mode; + } + } + + // Start assembling the package "source" directory. + // + // It's hard to predict all the files that will be generated (and + // potentially read), so we will just require a clean output directory. + // + // 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); + } + } + + // Normally the source directory is called - + // (e.g., as unpacked from the source archive). + // + dir_path src (out / dir_path (pn.string () + '-' + pv.string ())); + dir_path deb (src / dir_path ("debian")); + mk_p (deb); + + // The control file. + // + // See the "Control files and their fields" chapter in the Debian Policy + // Manual for details (for example, which fields are mandatory). + // + // Note that we try to do a reasonably thorough job (e.g., filling in + // sections, etc) with the view that this can be used as a starting point + // for manual packaging (and perhaps we could add a mode for this in the + // future, call it "starting point" mode). + // + // Also note that this file supports variable substitutions (for example, + // ${binary:Version}) as described in deb-substvars(5). While we could do + // without, it is widely used in manual packages so we do the same. Note, + // however, that we don't use the shlibs:Depends/misc:Depends mechanism + // (which automatically detects dependencies) since we have an accurate + // set and some of them may not be system packages. + // + string homepage (pm.package_url ? pm.package_url->string () : + pm.url ? pm.url->string () : + string ()); + + string maintainer; + if (ops_->debian_maintainer_specified ()) + maintainer = ops_->debian_maintainer (); + else + { + const email* e (pm.package_email ? &*pm.package_email : + pm.email ? &*pm.email : + nullptr); + + if (e == nullptr) + fail << "unable to determine package maintainer from manifest" << + info << "specify explicitly with --debian-maintainer"; + + // 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; + } + + path ctrl (deb / "control"); + try + { + ofdstream os (ctrl); + + // First comes the general (source package) stanza. + // + // Note that the Priority semantics is not the same as our priority. + // Rather it should reflect the overall importance of the package. Our + // priority is more appropriately mapped to urgency in the changelog. + // + // If this is not a library, then by default we assume its some kind of + // a development tool and use the devel section. + // + // Note also that we require the debhelper compatibility level 13 which + // has more advanced features that we rely on. Such as: + // + // - Variable substitutions in the debhelper config files. + // + string section ( + ops_->debian_section_specified () ? ops_->debian_section () : + lib ? "libs" : + "devel"); + + string priority ( + ops_->debian_priority_specified () ? ops_->debian_priority () : + "optional"); + + os << "Source: " << pn << '\n' + << "Section: " << section << '\n' + << "Priority: " << priority << '\n' + << "Maintainer: " << maintainer << '\n' + << "Standards-Version: " << "4.6.2" << '\n' + << "Build-Depends: " << "debhelper-compat (= 13)" << '\n' + << "Rules-Requires-Root: " << "no" << '\n'; + if (!homepage.empty ()) + os << "Homepage: " << homepage << '\n'; + if (pm.src_url) + os << "Vcs-Browser: " << pm.src_url->string () << '\n'; + + // Then we have one or more binary package stanzas. + // + // Note that values from the source package stanza (such as Section, + // Priority) are used as defaults for the binary packages. + // + // We cannot easily detect architecture-independent packages (think + // libbutl.bash) and providing an option feels like the best we can do. + // Note that the value `any` means architecture-dependent while `all` + // means architecture-independent. + // + // The Multi-Arch hint can be `same` or `foreign`. The former means that + // a separate copy of the package may be installed for each architecture + // (e.g., library) while the latter -- that a single copy may be used by + // all architectures (e.g., executable, -doc, -common). Not that for + // some murky reasons Multi-Arch:foreign needs to be explicitly + // specified for Architecture:all. + // + // The Description field is quite messy: it requires both the short + // description (our summary) as a first line and a long description (our + // description) as the following lines in the multiline format. + // Converting our description to the Debian format is not going to be + // easy: it can be arbitrarily long and may not even be plain text (it's + // commonly the contents of the README.md file). So for now we fake it + // with a description of the package component. Note also that + // traditionally the Description field comes last. + // + string arch (ops_->debian_architecture_specified () + ? ops_->debian_architecture () + : "any"); + + string march (arch == "all" || !lib ? "foreign" : "same"); + + { + string depends; + + if (!st.common.empty ()) + depends = st.common + " (= ${binary:Version})"; + + for (const package_status& st: sdeps) + { + if (!depends.empty ()) + depends += ", "; + + // Note that the constraints will include build metadata (e.g., + // ~debian10). While it may be tempting to strip it, we cannot since + // the order is inverse. We could just make it empty `~`, though + // that will look a bit strange. But keeping it shouldn't cause any + // issues. Also note that the build metadata is part of the revision + // so we could strip the whole thing. + // + depends += st.main + " (>= " + st.system_version + ')'; + } + + if (ops_->debian_main_langdep_specified ()) + { + if (!ops_->debian_main_langdep ().empty ()) + { + if (!depends.empty ()) + depends += ", "; + + depends += ops_->debian_main_langdep (); + } + } + else + { + // Note that we are not going to add dependencies on libcN + // (currently libc6) or libstdc++N (currently libstdc++6) because + // it's not easy to determine N and they both are normally part of + // the base system. + // + // What about other language runtimes? Well, it doesn't seem like we + // can deduce those automatically so we will either have to add ad + // hoc support or the user will have to provide them manually with + // --debian-main-depends. + } + + if (!ops_->debian_main_extradep ().empty ()) + { + if (!depends.empty ()) + depends += ", "; + + depends += ops_->debian_main_extradep (); + } + + os << '\n' + << "Package: " << st.main << '\n' + << "Architecture: " << arch << '\n' + << "Multi-Arch: " << march << '\n'; + if (!depends.empty ()) + os << "Depends: " << depends << '\n'; + os << "Description: " << pm.summary << '\n' + << " This package contains the runtime files." << '\n'; + } + + if (!st.dev.empty ()) + { + string depends (st.main + " (= ${binary:Version})"); + + for (const package_status& st: sdeps) + { + // Doesn't look like we can distinguish between interface and + // implementation dependencies here. So better to over- than + // under-specify. + // + if (!st.dev.empty ()) + depends += ", " + st.dev + " (>= " + st.system_version + ')'; + } + + if (ops_->debian_dev_langdep_specified ()) + { + if (!ops_->debian_dev_langdep ().empty ()) + { + depends += ", " + ops_->debian_dev_langdep (); + } + } + else + { + // Add dependency on libcN-dev and libstdc++-N-dev. + // + // Note: libcN-dev provides libc-dev and libstdc++N-dev provides + // libstdc++-dev. While it would be better to depend on the exact + // versions, determining N is not easy (and in case of listdc++ + // there could be multiple installed at the same time). + // + // Note that we haven't seen just libc-dev in any native packages, + // it's always either libc6-dev or libc6-dev|libc-dev. So we will + // see how it goes. + // + // If this is an undetermined C-common library, we assume it may be + // C++ (better to over- than under-specify). + // + bool cc (lang ("cc", true)); + if (cc || (cc = lang ("c++", true))) depends += ", libstdc++-dev"; + if (cc || (cc = lang ("c", true))) depends += ", libc-dev"; + } + + if (!ops_->debian_dev_extradep ().empty ()) + { + depends += ", " + ops_->debian_dev_extradep (); + } + + // Feels like the architecture should be the same as for the main + // package. + // + os << '\n' + << "Package: " << st.dev << '\n' + << "Section: " << (lib ? "libdevel" : "devel") << '\n' + << "Architecture: " << arch << '\n' + << "Multi-Arch: " << march << '\n'; + if (!st.doc.empty ()) + os << "Suggests: " << st.doc << '\n'; + if (!depends.empty ()) + os << "Depends: " << depends << '\n'; + os << "Description: " << pm.summary << '\n' + << " This package contains the development files." << '\n'; + } + + if (!st.doc.empty ()) + { + os << '\n' + << "Package: " << st.doc << '\n' + << "Section: " << "doc" << '\n' + << "Architecture: " << "all" << '\n' + << "Multi-Arch: " << "foreign" << '\n' + << "Description: " << pm.summary << '\n' + << " This package contains the documentation." << '\n'; + } + + // Keep this in case we want to support it in the "starting point" mode. + // + if (!st.dbg.empty ()) + { + string depends (st.main + " (= ${binary:Version})"); + + os << '\n' + << "Package: " << st.dbg << '\n' + << "Section: " << "debug" << '\n' + << "Priority: " << "extra" << '\n' + << "Architecture: " << arch << '\n' + << "Multi-Arch: " << march << '\n'; + if (!depends.empty ()) + os << "Depends: " << depends << '\n'; + os << "Description: " << pm.summary << '\n' + << " This package contains the debugging information." << '\n'; + } + + if (!st.common.empty ()) + { + // Generally, this package is not necessarily architecture-independent + // (for example, it could contain something shared between multiple + // binary packages produced from the same source package rather than + // something shared between all the architectures of a binary + // package). But seeing that we always generate one binary package, + // for us it only makes sense as architecture-independent. + // + // It's also not clear what dependencies we can deduce for this + // package. Assuming that it depends on all the dependency -common + // packages is probably unreasonable. + // + os << '\n' + << "Package: " << st.common << '\n' + << "Architecture: " << "all" << '\n' + << "Multi-Arch: " << "foreign" << '\n' + << "Description: " << pm.summary << '\n' + << " This package contains the architecture-independent files." << '\n'; + } + + os.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << ctrl << ": " << e; + } + + // The changelog file. + // + // See the "Debian changelog" section in the Debian Policy Manual for + // details. + // + // In particular, this is the sole source of the package version. + // + timestamp now (system_clock::now ()); + + path chlog (deb / "changelog"); + try + { + ofdstream os (chlog); + + // The first line has the following format: + // + // () ; urgency= + // + // Note that doesn't end up in the binary package. + // Normally all Debian packages start in unstable or experimental. + // + string urgency; + switch (pm.priority ? pm.priority->value : priority::low) + { + case priority::low: urgency = "low"; break; + case priority::medium: urgency = "medium"; break; + case priority::high: urgency = "high"; break; + case priority::security: urgency = "critical"; break; + } + + os << pn << " (" << st.system_version << ") " + << (pv.release ? "experimental" : "unstable") << "; " + << "urgency=" << urgency << '\n'; + + // Next we have a bunch of "change details" lines that start with `*` + // indented with two spaces. They are traditionally seperated from the + // first and last lines with blank lines. + // + os << '\n' + << " * New bpkg package release " << pv.string () << '.' << '\n' + << '\n'; + + // The last line is the "maintainer signoff" and has the following + // form: + // + // -- + // + // The component shall have the following form in the English + // locale (Mon, Jan, etc): + // + // ,
:: + + // + timestamp now (system_clock::now ()); + os << " -- " << maintainer << " "; + std::locale l (os.imbue (std::locale ("C"))); + to_stream (os, + now, + "%a, %d %b %Y %T %z", + false /* special */, + true /* local */); + os.imbue (l); + os << '\n'; + + os.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << chlog << ": " << e; + } + + // The copyright file. + // + // See the "Machine-readable debian/copyright file" document for + // details. + // + // Note that while not entirely clear, it looks like there should be at + // least one Files stanza. + // + // Note also that there is currently no way for us to get accurate + // copyright information. + // + // @@ TODO: Strictly speaking, in the recursive mode, we should collect + // licenses of all the dependencies we are bundling. + // + path copyr (deb / "copyright"); + try + { + ofdstream os (copyr); + + string license; + for (const licenses& ls: pm.license_alternatives) + { + if (!license.empty ()) + license += " or "; + + for (auto b (ls.begin ()), i (b); i != ls.end (); ++i) + { + if (i != b) + license += " and "; + + license += *i; + } + } + + os << "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/" << '\n' + << "Upstream-Name: " << pn << '\n' + << "Upstream-Contact: " << maintainer << '\n'; + if (!homepage.empty ()) + os << "Source: " << homepage << '\n'; + os << "License: " << license << '\n' + << "Comment: See accompanying files for exact copyright information" << '\n' + << " and full license text(s)." << '\n'; + + // Note that for licenses mentioned in the Files stanza we either have + // to provide the license text(s) inline or as separate License stanzas. + // + os << '\n' + << "Files: *" << '\n' + << "Copyright: "; + to_stream (os, now, "%Y", false /* special */, true /* local */); + os << " the " << pn << " authors (see accompanying files for details)" << '\n' + << "License: " << license << '\n' + << " See accompanying files for full license text(s)." << '\n'; + + os.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << copyr << ": " << e; + } + + // The source/format file. + // + dir_path deb_src (deb / dir_path ("source")); + mk (deb_src); + + path format (deb_src / "format"); + try + { + ofdstream os (format); + os << "3.0 (quilt)\n"; + os.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << format << ": " << e; + } + + // The rules makefile. Note that it must be executable. + // + // This file is executed by dpkg-buildpackage(1) which expects it to + // provide the following "API" make targets: + // + // clean + // + // build -- configure and build for all package + // build-arch -- configure and build for Architecture:any packages + // build-indep -- configure and build for Architecture:all packages + // + // binary -- make all binary packages + // binary-arch -- make Architecture:any binary packages + // binary-indep -- make Architecture:all binary packages + // + // The dh command sequencer provides the standard implementation of these + // API targets with the following customization point targets (for an + // overview of dh, start with the slides from the "Not Your Grandpa's + // Debhelper" presentation at DebConf 9 followed by the dh(1) man page): + // + // override_dh_auto_configure # ./configure --prefix=/usr + // override_dh_auto_build # make + // override_dh_auto_test # make test + // override_dh_auto_install # make install + // override_dh_auto_clean # make distclean + // + // Note that pretty much any dh_xxx command invoked by dh in order to + // implement the API targets can be customized with the corresponding + // override_dh_xxx target. To see what commands are executed for an API + // target, run `dh --no-act`. + // + path rules (deb / "rules"); + try + { + bool lang_c (lang ("c")); + bool lang_cxx (lang ("c++")); + bool lang_cc (lang ("cc")); + + // See fdopen() for details (umask, etc). + // + permissions ps (permissions::ru | permissions::wu | permissions::xu | + permissions::rg | permissions::wg | permissions::xg | + permissions::ro | permissions::wo | permissions::xo); + ofdstream os (fdopen (rules, + fdopen_mode::out | fdopen_mode::create, + ps)); + + os << "#!/usr/bin/make -f\n" + << "# -*- makefile -*-\n" + << '\n'; + + // See debhelper(7) for details on these. + // + // Note that there is also the DEB_BUILD_OPTIONS=terse option. Perhaps + // for the "starting point" mode we should base DH_* values as well as + // the build system verbosity below on that value. See debian/rules in + // upstream mariadb for what looks like a sensible setup. + // + if (verb == 0) + os << "export DH_QUIET := 1\n" + << '\n'; + else if (verb == 1) + os << "# Uncomment this to turn on verbose mode.\n" + << "#export DH_VERBOSE := 1\n" + << '\n'; + else + os << "export DH_VERBOSE := 1\n" + << '\n'; + + // We could have instead called dpkg-architecture directly but seeing + // that we are also include buildflags.mk below, might as well use + // architecture.mk (in the packages that we sampled you see both + // approaches). Note that these come in the dpkg-dev package, the same + // as dpkg-buildpackage. + // + os << "# DEB_HOST_* (DEB_HOST_MULTIARCH, etc)" << '\n' + << "#" << '\n' + << "include /usr/share/dpkg/architecture.mk" << '\n' + << '\n'; + + if (ops_->debian_buildflags () != "ignore") + { + // While we could have called dpkg-buildflags directly, including + // buildflags.mk instead appears to be the standard practice. + // + // Note that theses flags are not limited to C-based languages (for + // example, they also cover Assembler, Fortran, and potentially others + // in the future). + // + string mo; // Include leading space if not empty. + if (ops_->debian_maint_option_specified ()) + { + for (const string& o: ops_->debian_maint_option ()) + { + if (!o.empty ()) + { + mo += ' '; + mo += o; + } + } + } + else + mo = " hardening=+all"; + + os << "# *FLAGS (CFLAGS, CXXFLAGS, etc)" << '\n' + << "#" << '\n' + << "export DEB_BUILD_MAINT_OPTIONS :=" << mo << '\n' + << "include /usr/share/dpkg/buildflags.mk" << '\n' + << '\n'; + + // Fixup -ffile-prefix-map option (if specified) which is used to + // strip source file path prefix in debug information (besides other + // places). By default it points to the source directory. We change it + // to point to the bpkg configuration directory. Note that this won't + // work for external packages with source out of configuration (e.g., + // managed by bdep). + // + if (lang_c || lang_cc) + { + // @@ TODO: OBJCFLAGS. + + os << "CFLAGS := $(patsubst -ffile-prefix-map=%,-ffile-prefix-map=" + << cfg_dir.string () << "=.,$(CFLAGS))" << '\n' + << '\n'; + } + + if (lang_cxx || lang_cc) + { + // @@ TODO: OBJCXXFLAGS. + + os << "CXXFLAGS := $(patsubst -ffile-prefix-map=%,-ffile-prefix-map=" + << cfg_dir.string () << "=.,$(CXXFLAGS))" << '\n' + << '\n'; + } + } + + // The debian/tmp/ subdirectory appears to be the canonical destination + // directory (see dh_auto_install(1) for details). + // + os << "DESTDIR := $(CURDIR)/debian/tmp" << '\n' + << '\n'; + + // Let's use absolute path to the build system driver in case we are + // invoked with altered environment or some such. + // + // See --jobs documentation in dpkg-buildpackage(1) for details on + // parallel=N. + // + // Note: should be consistent with the invocation in installed_entries() + // above. + // + cstrings verb_args; string verb_arg; + map_verb_b (*ops_, verb_b::normal, verb_args, verb_arg); + + os << "b := " << search_b (*ops_).effect_string (); + for (const char* o: verb_args) os << ' ' << o; + for (const string& o: ops_->build_option ()) os << ' ' << o; + os << '\n' + << '\n' + << "parallel := $(filter parallel=%,$(DEB_BUILD_OPTIONS))" << '\n' + << "ifneq ($(parallel),)" << '\n' + << " parallel := $(patsubst parallel=%,%,$(parallel))" << '\n' + << " ifeq ($(parallel),1)" << '\n' + << " b += --serial-stop" << '\n' + << " else" << '\n' + << " b += --jobs=$(parallel)" << '\n' + << " endif" << '\n' + << "endif" << '\n' + << '\n'; + + // Configuration variables. + // + // Note: we need to quote values that contain `<>`, `[]`, since they + // will be passed through shell. For simplicity, let's just quote + // everything. + // + os << "config := config.install.chroot='$(DESTDIR)/'" << '\n' + << "config += config.install.sudo='[null]'" << '\n'; + + // If this is a C-based language, add rpath for private installation. + // + if (priv && (lang_c || lang_cxx || lang_cc)) + os << "config += config.bin.rpath='/usr/lib/$(DEB_HOST_MULTIARCH)/" + << pn << "/'" << '\n'; + + // Add build flags. + // + if (ops_->debian_buildflags () != "ignore") + { + const string& m (ops_->debian_buildflags ()); + + string o (m == "assign" ? "=" : + m == "append" ? "+=" : + m == "prepend" ? "=+" : ""); + + if (o.empty ()) + fail << "unknown --debian-buildflags option value '" << m << "'"; + + // Note that config.cc.* doesn't play well with the append/prepend + // modes because the orders are: + // + // x.poptions cc.poptions + // cc.coptions x.coptions + // cc.loptions x.loptions + // + // Oh, well, hopefully it will be close enough for most cases. + // + // Note also that there are compiler mode options that are not + // overridden. + // + if (o == "=" && (lang_c || lang_cxx || lang_cc)) + { + os << "config += config.cc.poptions='[null]'" << '\n' + << "config += config.cc.coptions='[null]'" << '\n' + << "config += config.cc.loptions='[null]'" << '\n'; + } + + if (lang_c || lang_cc) + { + // @@ TODO: OBJCFLAGS (we currently don't have separate options). + // Also see -ffile-prefix-map fixup above. + + os << "config += config.c.poptions" << o << "'$(CPPFLAGS)'" << '\n' + << "config += config.c.coptions" << o << "'$(CFLAGS)'" << '\n' + << "config += config.c.loptions" << o << "'$(LDFLAGS)'" << '\n'; + } + + if (lang_cxx || lang_cc) + { + // @@ TODO: OBJCXXFLAGS (we currently don't have separate options). + // Also see -ffile-prefix-map fixup above. + + os << "config += config.cxx.poptions" << o << "'$(CPPFLAGS)'" << '\n' + << "config += config.cxx.coptions" << o << "'$(CXXFLAGS)'" << '\n' + << "config += config.cxx.loptions" << o << "'$(LDFLAGS)'" << '\n'; + } + + // @@ TODO: ASFLAGS (when we have assembler support). + } + + // Keep last to allow user-specified configuration variables to override + // anything. + // + for (const string& c: config) + { + // Quote the value unless already quoted (see above). Presense of + // potentially-quoted user variables complicates things a bit (can + // be partially quoted, double-quoted, etc). + // + size_t p (c.find_first_of ("=+ \t")); // End of name. + if (p != string::npos) + { + p = c.find_first_not_of ("=+ \t", p); // Beginning of value. + if (p != string::npos) + { + if (c.find_first_of ("'\"", p) == string::npos) // Not quoted. + { + os << "config += " << string (c, 0, p) << '\'' + << string (c, p) << "'\n"; + continue; + } + } + } + + os << "config += " << c << '\n'; + } + + os << '\n'; + + // List of packages we need to install. + // + for (auto b (pkgs.begin ()), i (b); i != pkgs.end (); ++i) + { + os << "packages" << (i == b ? " := " : " += ") + << i->out_root.representation () << '\n'; + } + os << '\n'; + + // Disable synchronization hooks for good measure. + // + os << "export BDEP_SYNC := 0\n" + << '\n'; + + // Default to the dh command sequencer. + // + // Note that passing --buildsystem=none doesn't seem to make any + // difference (other than add some noise). + // + os << "%:\n" + << '\t' << "dh $@" << '\n' + << '\n'; + + // Override dh_auto_configure. + // + os << "# Everything is already configured.\n" + << "#\n" + << "override_dh_auto_configure:\n" + << '\n'; + + // Override dh_auto_build. + // + os << "override_dh_auto_build:\n" + << '\t' << "$b $(config) update-for-install: $(packages)" << '\n' + << '\n'; + + // Override dh_auto_test. + // + // Note that running tests after update-for-install may cause rebuild + // (e.g., relinking without rpath, etc) before tests and again before + // install. So doesn't seem worth the trouble. + // + os << "# Assume any testing has already been done.\n" + << "#\n" + << "override_dh_auto_test:\n" + << '\n'; + + // Override dh_auto_install. + // + os << "override_dh_auto_install:\n" + << '\t' << "$b $(config) '!config.install.scope=" << scope << "' " + << "install: $(packages)" << '\n' + << '\n'; + + // Override dh_auto_clean. + // + os << "# This is not a real source directory so nothing to clean.\n" + << "#\n" + << "override_dh_auto_clean:\n" + << '\n'; + + // Override dh_shlibdeps. + // + // Failed that we get a warning about calculated ${shlibs:Depends} being + // unused. + // + // Note that there is also dh_makeshlibs which is invoked just before + // but we shouldn't override it because (quoting its man page): "It will + // also ensure that ldconfig is invoked during install and removal when + // it finds shared libraries." + // + os << "# Disable dh_shlibdeps since we don't use ${shlibs:Depends}.\n" + << "#\n" + << "override_dh_shlibdeps:\n" + << '\n'; + + os.close (); + } + catch (const io_error& e) + { + fail << "unable to write to " << rules << ": " << e; + } + + // Generate the dh_install (.install) config files for each package in + // order to sort out which files belong where. + // + // For documentation of the config file format see debhelper(1) and + // dh_install(1). But the summary is: + // + // - Supports only simple wildcards (?, *, [...]; no recursive/**). + // - But can install whole directories recursively. + // - An entry that doesn't match anything is an error (say, /usr/sbin/*). + // - Supports variable substitutions (${...}; since compat level 13). + // + // Keep in mind that wherever there is in the config.install.* + // variable, we can end up with multiple different directories (bundled + // packages). + // + path main_install; + path dev_install; + path doc_install; + path dbg_install; + path common_install; + + const path* cur_install (nullptr); // File being opened/written to. + try + { + pair main (main_install, auto_fd ()); + pair dev (dev_install, auto_fd ()); + pair doc (doc_install, auto_fd ()); + pair dbg (dbg_install, auto_fd ()); + pair com (common_install, auto_fd ()); + + auto open = [&deb, &cur_install] (pair& os, + const string& n) + { + if (!n.empty ()) + { + cur_install = &(os.first = deb / (n + ".install")); + os.second.open (os.first); + } + }; + + open (main, st.main); + open (dev, st.dev); + open (doc, st.doc); + open (dbg, st.dbg); + open (com, st.common); + + auto is_open = [] (pair& os) + { + return os.second.is_open (); + }; + + auto add = [&cur_install] (pair& os, const path& p) + { + // Strip root. + // + string s (p.leaf (p.root_directory ()).string ()); + + // Replace () with {}. + // + for (char& c: s) + { + if (c == '(') c = '{'; + if (c == ')') c = '}'; + } + + cur_install = &os.first; + os.second << s << '\n'; + }; + + // Let's tighten things up and only look in / (if specified) to + // make sure there is nothing stray. + // + string pd (priv ? pn.string () + '/' : ""); + + // NOTE: keep consistent with the config.install.* values above. + // + dir_path bindir ("/usr/bin/"); + dir_path sbindir ("/usr/sbin/"); + dir_path etcdir ("/etc/"); + dir_path incdir ("/usr/include/" + pd); + dir_path incarchdir ("/usr/include/$(DEB_HOST_MULTIARCH)/" + pd); + dir_path libdir ("/usr/lib/$(DEB_HOST_MULTIARCH)/" + pd); + dir_path pkgdir (libdir / dir_path ("pkgconfig")); + dir_path sharedir ("/usr/share/" + pd); + dir_path docdir ("/usr/share/doc/" + pd); + dir_path mandir ("/usr/share/man/"); + + // The main package contains everything that doesn't go to another + // packages. + // + if (ies.contains_sub (bindir)) add (main, bindir / "*"); + if (ies.contains_sub (sbindir)) add (main, sbindir / "*"); + + // This could potentially go to -common but it could also be target- + // specific, who knows. So let's keep it in main for now. + // + if (ies.contains_sub (etcdir)) add (main, etcdir / "*"); + + if (!is_open (dev)) + { + if (ies.contains_sub (incdir)) add (main, incdir / "*"); + if (ies.contains_sub (incarchdir)) add (main, incarchdir / "*"); + if (ies.contains_sub (libdir)) add (main, libdir / "*"); + } + else + { + if (ies.contains_sub (incdir)) add (dev, incdir / "*"); + if (ies.contains_sub (incarchdir)) add (dev, incarchdir / "*"); + + // Ok, time for things to get hairy: we need to split the contents + // of lib/ into the main and -dev packages. The -dev package should + // contain three things: + // + // 1. Static libraries (.a). + // 2. Non-versioned shared library symlinks (.so). + // 3. Contents of the pkgconfig/ subdirectory. + // + // Everything else should go into the main package. In particular, we + // assume any subdirectories other than pkgconfig/ are the libexec + // stuff or similar. + // + // The (2) case (shared library) is tricky. Here we can have three + // plausible arrangements: + // + // A. Portably-versioned library: + // + // libfoo-1.2.so + // libfoo.so -> libfoo-1.2.so + // + // B. Natively-versioned library: + // + // libfoo.so.1.2.3 + // libfoo.so.1.2 -> libfoo.so.1.2.3 + // libfoo.so.1 -> libfoo.so.1.2 + // libfoo.so -> libfoo.so.1 + // + // C. Non-versioned library: + // + // libfoo.so + // + // Note that in the (C) case the library should go into the main + // package. Based on this, the criteria appears to be straightforwrad: + // the extension is .so and it's a symlink. For good measure we also + // check that there is the `lib` prefix (plugins, etc). + // + for (auto p (ies.find_sub (libdir)); p.first != p.second; ) + { + const path& f (p.first->first); + const installed_entry& ie ((p.first++)->second); + + path l (f.leaf (libdir)); + + if (l.simple ()) + { + string e (l.extension ()); + const string& n (l.string ()); + + bool d (n.size () > 3 && n.compare (0, 3, "lib") == 0 && + ((e == "a" ) || + (e == "so" && ie.target != nullptr))); + + add (d ? dev : main, libdir / l); + } + else + { + // Let's keep things tidy and use a wildcard rather than listing + // all the entries in subdirectories verbatim. + // + dir_path d (libdir / dir_path (*l.begin ())); + + add (d == pkgdir ? dev : main, d / "*"); + + // Skip all the other entries in this subdirectory (in the prefix + // map they will all be in a contiguous range). + // + while (p.first != p.second && p.first->first.sub (d)) + ++p.first; + } + } + } + + // We cannot just do usr/share/* since it will clash with doc/ and man/ + // below. So we have to list all the top-level entries in usr/share/ + // that are not doc/ or man/. + // + for (auto p (ies.find_sub (sharedir)); p.first != p.second; ) + { + const path& f ((p.first++)->first); + + if (f.sub (docdir) || f.sub (mandir)) + continue; + + path l (f.leaf (sharedir)); + + if (l.simple ()) + add (is_open (com) ? com : main, sharedir / l); + else + { + // Let's keep things tidy and use a wildcard rather than listing all + // the entries in subdirectories verbatim. + // + dir_path d (sharedir / dir_path (*l.begin ())); + + add (is_open (com) ? com : main, d / "*"); + + // Skip all the other entries in this subdirectory (in the prefix + // map they will all be in a contiguous range). + // + while (p.first != p.second && p.first->first.sub (d)) + ++p.first; + } + } + + // Should we put the documentation into -common if there is no -doc? + // While there doesn't seem to be anything explicit in the policy, there + // are packages that do it this way (e.g., libao, libaudit). And the + // same logic seems to apply to -dev (e.g., zlib). + // + { + auto& os (is_open (doc) ? doc : + is_open (com) ? com : + is_open (dev) ? dev : + main); + + if (ies.contains_sub (docdir)) add (os, docdir / "*"); + if (ies.contains_sub (mandir)) add (os, mandir / "*"); + } + + // Close. + // + auto close = [&cur_install] (pair& os) + { + if (os.second.is_open ()) + { + cur_install = &os.first; + os.second.close (); + } + }; + + close (main); + close (dev); + close (doc); + close (dbg); + close (com); + } + catch (const io_error& e) + { + fail << "unable to write to " << *cur_install << ": " << e; + } + + // Run dpkg-buildpackage. + // + // Note that there doesn't seem to be any way to control its verbosity or + // progress. + // + // Note also that dpkg-buildpackage causes recompilation on every run by + // changing the SOURCE_DATE_EPOCH environment variable (which we track for + // changes since it affects GCC). Note that since we don't have this + // SOURCE_DATE_EPOCH during dry-run caused by installed_entries(), there + // would be a recompilation even if the value weren't changing. + // + cstrings args { + "dpkg-buildpackage", + "--build=binary", // Only build binary packages. + "--no-sign", // Do not sign anything. + "--target-arch", arch.c_str ()}; + + // Pass our --jobs value, if any. + // + string jobs_arg; + if (size_t n = ops_->jobs_specified () ? ops_->jobs () : 0) + { + // Note: only accepts the --jobs=N form. + // + args.push_back ((jobs_arg = "--jobs=" + to_string (n)).c_str ()); + } + + // Pass any additional options specified by the user. + // + for (const string& o: ops_->debian_build_option ()) + args.push_back (o.c_str ()); + + args.push_back (nullptr); + + if (ops_->debian_prepare_only ()) + { + if (verb >= 1) + { + diag_record dr (text); + + dr << "prepared " << src << + text << "command line: "; + + print_process (dr, args); + } + + return paths {}; + } + + try + { + process_path pp (process::path_search (args[0])); + process_env pe (pp, src /* cwd */); + + // There is going to be quite a bit of diagnostics so print the command + // line unless quiet. + // + if (verb >= 1) + print_process (pe, args); + + // Redirect stdout to stderr since half of dpkg-buildpackage diagnostics + // goes there. For good measure also redirect stdin to /dev/null to make + // sure there are no prompts of any kind. + // + process pr (pp, + args, + -2 /* stdin */, + 2 /* stdout */, + 2 /* stderr */, + pe.cwd->string ().c_str (), + pe.vars); + + if (!pr.wait ()) + { + // Let's repeat the command line even if it was printed at the + // beginning to save the user a rummage through the logs. + // + diag_record dr (fail); + dr << args[0] << " exited with non-zero code" << + info << "command line: "; print_process (dr, pe, args); + } + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e; + + if (e.child) + exit (1); + + throw failed (); + } + + // Cleanup intermediate files unless requested not to. + // + if (!ops_->keep_output ()) + { + rm_r (src); + } + + // Collect and return the binary package paths. + // + paths r; + auto add = [&out, &r] (const string& n, bool opt = false) + { + path p (out / n); + + if (exists (p)) + r.push_back (move (p)); + else if (!opt) + fail << "expected output file " << p << " does not exist"; + }; + + // The resulting .deb file names have the __.deb + // form. If the package is architecture-independent, then is the + // special `all` value. + // + const string& ver (st.system_version); + + add (st.main + '_' + ver + '_' + arch + ".deb"); + add (st.main + "-dbgsym_" + ver + '_' + arch + ".deb", true); + if (!st.dev.empty ()) add (st.dev + '_' + ver + '_' + arch + ".deb"); + if (!st.doc.empty ()) add (st.doc + '_' + ver + "_all.deb"); + if (!st.common.empty ()) add (st.common + '_' + ver + "_all.deb"); + + // Besides the binary packages (.deb) we also get the .buildinfo and + // .changes files, which could be useful. Note that their names are based + // on the source package name. + // + add (pn.string () + '_' + ver + '_' + arch + ".buildinfo"); + add (pn.string () + '_' + ver + '_' + arch + ".changes"); + + return r; } } -- cgit v1.1