diff options
author | Karen Arutyunov <karen@codesynthesis.com> | 2019-08-01 16:10:48 +0300 |
---|---|---|
committer | Karen Arutyunov <karen@codesynthesis.com> | 2019-08-01 16:41:08 +0300 |
commit | da9cbf29c403d27c2940f9b31199c4648f8ae4a1 (patch) | |
tree | 9732de6f48bdaead2becf32be41b1284e1e83e00 /libbuild2 | |
parent | 8e69e09b7ec68377758c63092f9b254e95a0d7be (diff) |
Move version build system module to separate library
Diffstat (limited to 'libbuild2')
-rw-r--r-- | libbuild2/buildfile | 2 | ||||
-rw-r--r-- | libbuild2/version/buildfile | 66 | ||||
-rw-r--r-- | libbuild2/version/export.hxx | 34 | ||||
-rw-r--r-- | libbuild2/version/init.cxx | 407 | ||||
-rw-r--r-- | libbuild2/version/init.hxx | 28 | ||||
-rw-r--r-- | libbuild2/version/module.cxx | 15 | ||||
-rw-r--r-- | libbuild2/version/module.hxx | 64 | ||||
-rw-r--r-- | libbuild2/version/rule.cxx | 334 | ||||
-rw-r--r-- | libbuild2/version/rule.hxx | 52 | ||||
-rw-r--r-- | libbuild2/version/snapshot-git.cxx | 175 | ||||
-rw-r--r-- | libbuild2/version/snapshot.cxx | 39 | ||||
-rw-r--r-- | libbuild2/version/snapshot.hxx | 34 | ||||
-rw-r--r-- | libbuild2/version/utility.cxx | 81 | ||||
-rw-r--r-- | libbuild2/version/utility.hxx | 25 |
14 files changed, 1355 insertions, 1 deletions
diff --git a/libbuild2/buildfile b/libbuild2/buildfile index b015d21..ab95098 100644 --- a/libbuild2/buildfile +++ b/libbuild2/buildfile @@ -2,7 +2,7 @@ # copyright : Copyright (c) 2014-2019 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -./: lib{build2} in/ +./: lib{build2} version/ in/ import int_libs = libbutl%lib{butl} diff --git a/libbuild2/version/buildfile b/libbuild2/version/buildfile new file mode 100644 index 0000000..e9d4905 --- /dev/null +++ b/libbuild2/version/buildfile @@ -0,0 +1,66 @@ +# file : libbuild2/version/buildfile +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import int_libs = libbutl%lib{butl} + +include ../ +int_libs += ../lib{build2} + +include ../in/ +int_libs += ../in/lib{build2-in} + +./: lib{build2-version}: libul{build2-version}: \ + {hxx ixx txx cxx}{** -**.test...} \ + $int_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} + $d/exe{$n}: libul{build2-version}: bin.whole = false +} + +# Build options. +# +obja{*}: cxx.poptions += -DLIBBUILD2_VERSION_STATIC_BUILD +objs{*}: cxx.poptions += -DLIBBUILD2_VERSION_SHARED_BUILD + +# Export options. +# +lib{build2-version}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $int_libs +} + +liba{build2-version}: cxx.export.poptions += -DLIBBUILD2_VERSION_STATIC +libs{build2-version}: cxx.export.poptions += -DLIBBUILD2_VERSION_SHARED + +# For pre-releases use the complete version to make sure they cannot be used +# in place of another pre-release or the final version. See the version module +# for details on the version.* variable values. +# +if $version.pre_release + lib{build2-version}: bin.lib.version = @"-$version.project_id" +else + lib{build2-version}: bin.lib.version = @"-$version.major.$version.minor" + +# Install into the libbuild2/version/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/libbuild2/version/ + install.subdirs = true +} diff --git a/libbuild2/version/export.hxx b/libbuild2/version/export.hxx new file mode 100644 index 0000000..c76cd8a --- /dev/null +++ b/libbuild2/version/export.hxx @@ -0,0 +1,34 @@ +#pragma once + +// Normally we don't export class templates (but do complete specializations), +// inline functions, and classes with only inline member functions. Exporting +// classes that inherit from non-exported/imported bases (e.g., std::string) +// will end up badly. The only known workarounds are to not inherit or to not +// export. Also, MinGW GCC doesn't like seeing non-exported functions being +// used before their inline definition. The workaround is to reorder code. In +// the end it's all trial and error. + +#if defined(LIBBUILD2_VERSION_STATIC) // Using static. +# define LIBBUILD2_VERSION_SYMEXPORT +#elif defined(LIBBUILD2_VERSION_STATIC_BUILD) // Building static. +# define LIBBUILD2_VERSION_SYMEXPORT +#elif defined(LIBBUILD2_VERSION_SHARED) // Using shared. +# ifdef _WIN32 +# define LIBBUILD2_VERSION_SYMEXPORT __declspec(dllimport) +# else +# define LIBBUILD2_VERSION_SYMEXPORT +# endif +#elif defined(LIBBUILD2_VERSION_SHARED_BUILD) // Building shared. +# ifdef _WIN32 +# define LIBBUILD2_VERSION_SYMEXPORT __declspec(dllexport) +# else +# define LIBBUILD2_VERSION_SYMEXPORT +# endif +#else +// If none of the above macros are defined, then we assume we are being used +// by some third-party build system that cannot/doesn't signal the library +// type. Note that this fallback works for both static and shared but in case +// of shared will be sub-optimal compared to having dllimport. +// +# define LIBBUILD2_VERSION_SYMEXPORT // Using static or shared. +#endif diff --git a/libbuild2/version/init.cxx b/libbuild2/version/init.cxx new file mode 100644 index 0000000..8adbb29 --- /dev/null +++ b/libbuild2/version/init.cxx @@ -0,0 +1,407 @@ +// file : libbuild2/version/init.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/version/init.hxx> + +#include <libbutl/manifest-parser.mxx> + +#include <libbuild2/scope.hxx> +#include <libbuild2/context.hxx> +#include <libbuild2/variable.hxx> +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/config/utility.hxx> + +#include <libbuild2/dist/module.hxx> + +#include <libbuild2/version/rule.hxx> +#include <libbuild2/version/module.hxx> +#include <libbuild2/version/utility.hxx> +#include <libbuild2/version/snapshot.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace version + { + static const path manifest_file ("manifest"); + + static const in_rule in_rule_; + static const manifest_install_rule manifest_install_rule_; + + bool + boot (scope& rs, const location& l, unique_ptr<module_base>& mod) + { + tracer trace ("version::boot"); + l5 ([&]{trace << "for " << rs;}); + + // Extract the version from the manifest file. As well as summary and + // url while at it. + // + // Also, as a sanity check, verify the package name matches the build + // system project name. + // + string sum; + string url; + + standard_version v; + dependencies ds; + { + path f (rs.src_path () / manifest_file); + + try + { + if (!file_exists (f)) + fail (l) << "no manifest file in " << rs.src_path (); + + ifdstream ifs (f); + manifest_parser p (ifs, f.string ()); + + manifest_name_value nv (p.next ()); + if (!nv.name.empty () || nv.value != "1") + fail (l) << "unsupported manifest format in " << f; + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + if (nv.name == "name") + { + auto& pn (cast<project_name> (rs.vars[var_project])); + + if (nv.value != pn.string ()) + { + path bf (rs.src_path () / rs.root_extra->bootstrap_file); + location ml (&f, nv.value_line, nv.value_column); + location bl (&bf); + + fail (ml) << "package name " << nv.value << " does not match " + << "build system project name " << pn << + info (bl) << "build system project name specified here"; + } + } + if (nv.name == "summary") + sum = move (nv.value); + else if (nv.name == "url") + url = move (nv.value); + else if (nv.name == "version") + { + try + { + // Allow the package stub versions in the 0+<revision> form. + // While not standard, we want to use the version module for + // packaging stubs. + // + v = standard_version (nv.value, standard_version::allow_stub); + } + catch (const invalid_argument& e) + { + fail << "invalid standard version '" << nv.value << "': " << e; + } + } + else if (nv.name == "depends") + { + // According to the package manifest spec, the format of the + // 'depends' value is as follows: + // + // depends: [?][*] <alternatives> [; <comment>] + // + // <alternatives> := <dependency> [ '|' <dependency>]* + // <dependency> := <name> [<constraint>] + // <constraint> := <comparison> | <range> + // <comparison> := ('==' | '>' | '<' | '>=' | '<=') <version> + // <range> := ('(' | '[') <version> <version> (')' | ']') + // + // Note that we don't do exhaustive validation here leaving it + // to the package manager. + // + string v (move (nv.value)); + + size_t p; + + // Get rid of the comment. + // + if ((p = v.find (';')) != string::npos) + v.resize (p); + + // Get rid of conditional/runtime markers. Note that enither of + // them is valid in the rest of the value. + // + if ((p = v.find_last_of ("?*")) != string::npos) + v.erase (0, p + 1); + + // Parse as |-separated "words". + // + for (size_t b (0), e (0); next_word (v, b, e, '|'); ) + { + string d (v, b, e - b); + trim (d); + + p = d.find_first_of (" \t=<>[(~^"); + string n (d, 0, p); + string c (p != string::npos ? string (d, p) : string ()); + + trim (n); + trim (c); + + try + { + package_name pn (move (n)); + string v (pn.variable ()); + + ds.emplace (move (v), dependency {move (pn), move (c)}); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid package name for dependency " + << d << ": " << e; + } + } + } + } + } + catch (const manifest_parsing& e) + { + location l (&f, e.line, e.column); + fail (l) << e.description; + } + catch (const io_error& e) + { + fail (l) << "unable to read from " << f << ": " << e; + } + catch (const system_error& e) // EACCES, etc. + { + fail (l) << "unable to access manifest " << f << ": " << e; + } + + if (v.empty ()) + fail (l) << "no version in " << f; + } + + // If this is the latest snapshot (i.e., the -a.1.z kind), then load the + // snapshot number and id (e.g., commit date and id from git). + // + bool committed (true); + bool rewritten (false); + if (v.snapshot () && v.snapshot_sn == standard_version::latest_sn) + { + snapshot ss (extract_snapshot (rs)); + + if (!ss.empty ()) + { + v.snapshot_sn = ss.sn; + v.snapshot_id = move (ss.id); + committed = ss.committed; + rewritten = true; + } + else + committed = false; + } + + // If there is a dependency on the build system itself, check it (so + // there is no need for explicit using build@X.Y.Z). + // + { + auto i (ds.find ("build2")); + + if (i != ds.end () && !i->second.constraint.empty ()) + try + { + check_build_version ( + standard_version_constraint (i->second.constraint, v), l); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid version constraint for dependency build2 " + << i->second.constraint << ": " << e; + } + } + + // Set all the version.* variables. + // + auto& vp (var_pool.rw (rs)); + + auto set = [&vp, &rs] (const char* var, auto val) + { + using T = decltype (val); + auto& v (vp.insert<T> (var, variable_visibility::project)); + rs.assign (v) = move (val); + }; + + if (!sum.empty ()) rs.assign (var_project_summary) = move (sum); + if (!url.empty ()) rs.assign (var_project_url) = move (url); + + set ("version", v.string ()); // Project version (var_version). + + set ("version.project", v.string_project ()); + set ("version.project_number", v.version); + + // Enough of project version for unique identification (can be used in + // places like soname, etc). + // + set ("version.project_id", v.string_project_id ()); + + set ("version.stub", v.stub ()); // bool + + set ("version.epoch", uint64_t (v.epoch)); + + set ("version.major", uint64_t (v.major ())); + set ("version.minor", uint64_t (v.minor ())); + set ("version.patch", uint64_t (v.patch ())); + + optional<uint16_t> a (v.alpha ()); + optional<uint16_t> b (v.beta ()); + + set ("version.alpha", a.has_value ()); + set ("version.beta", b.has_value ()); + set ("version.pre_release", v.pre_release ().has_value ()); + set ("version.pre_release_string", v.string_pre_release ()); + set ("version.pre_release_number", uint64_t (a ? *a : b ? *b : 0)); + + set ("version.snapshot", v.snapshot ()); // bool + set ("version.snapshot_sn", v.snapshot_sn); // uint64 + set ("version.snapshot_id", v.snapshot_id); // string + set ("version.snapshot_string", v.string_snapshot ()); + set ("version.snapshot_committed", committed); // bool + + set ("version.revision", uint64_t (v.revision)); + + // Create the module. + // + mod.reset (new module (cast<project_name> (rs.vars[var_project]), + move (v), + committed, + rewritten, + move (ds))); + + return true; // Init first (dist.package, etc). + } + + static void + dist_callback (const path&, const scope&, void*); + + bool + init (scope& rs, + scope&, + const location& l, + unique_ptr<module_base>& mod, + bool first, + bool, + const variable_map&) + { + tracer trace ("version::init"); + + if (!first) + fail (l) << "multiple version module initializations"; + + // Load in.base (in.* variables, in{} target type). + // + if (!cast_false<bool> (rs["in.base.loaded"])) + load_module (rs, rs, "in.base", l); + + module& m (static_cast<module&> (*mod)); + const standard_version& v (m.version); + + // If the dist module is used, set its dist.package and register the + // post-processing callback. + // + if (auto* dm = rs.lookup_module<dist::module> (dist::module::name)) + { + // Make sure dist is init'ed, not just boot'ed. + // + if (!cast_false<bool> (rs["dist.loaded"])) + load_module (rs, rs, "dist", l); + + m.dist_uncommitted = cast_false<bool> (rs["config.dist.uncommitted"]); + + // Don't touch if dist.package was set by the user. + // + value& val (rs.assign (dm->var_dist_package)); + + if (!val) + { + string p (cast<project_name> (rs.vars[var_project]).string ()); + p += '-'; + p += v.string (); + val = move (p); + + // Only register the post-processing callback if this is a rewritten + // snapshot. + // + if (m.rewritten) + dm->register_callback (dir_path (".") / manifest_file, + &dist_callback, + &m); + } + } + + // Register rules. + // + { + auto& r (rs.rules); + + r.insert<file> (perform_update_id, "version.in", in_rule_); + r.insert<file> (perform_clean_id, "version.in", in_rule_); + r.insert<file> (configure_update_id, "version.in", in_rule_); + + if (cast_false<bool> (rs["install.booted"])) + { + r.insert<manifest> ( + perform_install_id, "version.manifest", manifest_install_rule_); + } + } + + return true; + } + + static void + dist_callback (const path& f, const scope& rs, void* data) + { + module& m (*static_cast<module*> (data)); + + // Complain if this is an uncommitted snapshot. + // + if (!m.committed && !m.dist_uncommitted) + fail << "distribution of uncommitted project " << rs.src_path () << + info << "specify config.dist.uncommitted=true to force"; + + // The plan is simple: fixing up the version in a temporary file then + // move it to the original. + // + try + { + auto_rmfile t (fixup_manifest (f, + path::temp_path ("manifest"), + m.version)); + + mvfile (t.path, f, (cpflags::overwrite_content | + cpflags::overwrite_permissions)); + t.cancel (); + } + catch (const io_error& e) + { + fail << "unable to overwrite " << f << ": " << e; + } + catch (const system_error& e) // EACCES, etc. + { + fail << "unable to overwrite " << f << ": " << e; + } + } + + static const module_functions mod_functions[] = + { + // NOTE: don't forget to also update the documentation in init.hxx if + // changing anything here. + + {"version", boot, init}, + {nullptr, nullptr, nullptr} + }; + + const module_functions* + build2_version_load () + { + return mod_functions; + } + } +} diff --git a/libbuild2/version/init.hxx b/libbuild2/version/init.hxx new file mode 100644 index 0000000..68e4def --- /dev/null +++ b/libbuild2/version/init.hxx @@ -0,0 +1,28 @@ +// file : libbuild2/version/init.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_VERSION_INIT_HXX +#define LIBBUILD2_VERSION_INIT_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/module.hxx> + +#include <libbuild2/version/export.hxx> + +namespace build2 +{ + namespace version + { + // Module `version` requires bootstrapping. + // + // No submodules. + // + extern "C" LIBBUILD2_VERSION_SYMEXPORT const module_functions* + build2_version_load (); + } +} + +#endif // LIBBUILD2_VERSION_INIT_HXX diff --git a/libbuild2/version/module.cxx b/libbuild2/version/module.cxx new file mode 100644 index 0000000..5ee44f7 --- /dev/null +++ b/libbuild2/version/module.cxx @@ -0,0 +1,15 @@ +// file : libbuild2/version/module.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/version/module.hxx> + +using namespace std; + +namespace build2 +{ + namespace version + { + const string module::name ("version"); + } +} diff --git a/libbuild2/version/module.hxx b/libbuild2/version/module.hxx new file mode 100644 index 0000000..174da7b --- /dev/null +++ b/libbuild2/version/module.hxx @@ -0,0 +1,64 @@ +// file : libbuild2/version/module.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_VERSION_MODULE_HXX +#define LIBBUILD2_VERSION_MODULE_HXX + +#include <map> + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/module.hxx> + +namespace build2 +{ + namespace version + { + // A map of package names sanitized for use in variable names to the + // 'depends' values from manifest. + // + using package_name = project_name; + + struct dependency + { + package_name name; + string constraint; + }; + + using dependencies = std::map<string, dependency>; + + struct module: module_base + { + using dependencies_type = version::dependencies; + + static const string name; + + // The project variable value sanitized for use in variable names. + // + const string project; + + butl::standard_version version; + bool committed; // Whether this is a committed snapshot. + bool rewritten; // Whether this is a rewritten .z snapshot. + + dependencies_type dependencies; + + bool dist_uncommitted = false; + + module (const project_name& p, + butl::standard_version v, + bool c, + bool r, + dependencies_type d) + : project (p.variable ()), + version (move (v)), + committed (c), + rewritten (r), + dependencies (move (d)) {} + }; + } +} + +#endif // LIBBUILD2_VERSION_MODULE_HXX diff --git a/libbuild2/version/rule.cxx b/libbuild2/version/rule.cxx new file mode 100644 index 0000000..37e6b0f --- /dev/null +++ b/libbuild2/version/rule.cxx @@ -0,0 +1,334 @@ +// file : libbuild2/version/rule.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/version/rule.hxx> + +#include <libbuild2/scope.hxx> +#include <libbuild2/target.hxx> +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/in/target.hxx> + +#include <libbuild2/version/module.hxx> +#include <libbuild2/version/utility.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace version + { + using in::in; + + // Return true if this prerequisite is a project's manifest file. To be + // sure we would need to search it into target but that we can't do in + // match(). + // + static inline bool + manifest_prerequisite (const scope& rs, const prerequisite_member& p) + { + if (!p.is_a<manifest> () || p.name () != "manifest") + return false; + + const scope& s (p.scope ()); + + if (s.root_scope () == nullptr) // Out of project prerequisite. + return false; + + dir_path d (p.dir ()); + if (d.relative ()) + d = s.src_path () / d; + d.normalize (); + + return d == rs.src_path (); + } + + // in_rule + // + bool in_rule:: + match (action a, target& xt, const string&) const + { + tracer trace ("version::in_rule::match"); + + file& t (static_cast<file&> (xt)); + const scope& rs (t.root_scope ()); + + bool fm (false); // Found manifest. + bool fi (false); // Found in. + for (prerequisite_member p: group_prerequisite_members (a, t)) + { + if (include (a, t, p) != include_type::normal) // Excluded/ad hoc. + continue; + + fm = fm || manifest_prerequisite (rs, p); + fi = fi || p.is_a<in> (); + } + + // Note that while normally we print these at verbosity level 4, these + // ones get quite noisy since we try this rule any file target. + // + if (!fm) + l5 ([&]{trace << "no manifest prerequisite for target " << t;}); + + if (!fi) + l5 ([&]{trace << "no in file prerequisite for target " << t;}); + + bool r (fm && fi); + + // If we match, lookup and cache the module for the update operation. + // + if (r && a == perform_update_id) + t.data (rs.lookup_module<module> (module::name)); + + return r; + } + + string in_rule:: + lookup (const location& l, + action a, + const target& t, + const string& n) const + { + // Note that this code will be executed during up-to-date check for each + // substitution so let's try not to do anything overly sub-optimal here. + // + const module& m (*t.data<const module*> ()); + + // Split it into the package name and the variable/condition name. + // + // We used to bail if there is no package component but now we treat it + // the same as project. This can be useful when trying to reuse existing + // .in files (e.g., from autoconf, etc). + // + size_t p (n.find ('.')); + + if (p == string::npos || n.compare (0, p, m.project) == 0) + { + return rule::lookup (l, // Standard lookup. + a, + t, + p == string::npos ? n : string (n, p + 1)); + } + + string pn (n, 0, p); + string vn (n, p + 1); + + // Perform substitutions for a dependency. Here we recognize the + // following substitutions: + // + // $libfoo.version$ - textual version constraint. + // $libfoo.condition(VER[,SNAP])$ - numeric satisfaction condition. + // $libfoo.check(VER[,SNAP])$ - numeric satisfaction check (#if ...). + // + // Where VER is the version number macro and SNAP is the optional + // snapshot number macro (only needed if you plan to include snapshot + // informaton in your constraints). + // + // Note also that the last two (condition and check) can only be used in + // the strict substitution mode since in::rule::substitute() will skip + // them in the lax mode. + + // For now we re-parse the constraint every time. Firstly because not + // all of them are necessarily in the standard form and secondly because + // of the MT-safety. + // + standard_version_constraint dc; + const package_name* dn; + { + auto i (m.dependencies.find (pn)); + + if (i == m.dependencies.end ()) + fail (l) << "unknown dependency '" << pn << "'"; + + const dependency& dp (i->second); + + if (dp.constraint.empty ()) + fail (l) << "no version constraint for dependency " << dp.name; + + try + { + dc = standard_version_constraint (dp.constraint, m.version); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid version constraint for dependency " << dp.name + << " " << dp.constraint << ": " << e; + } + + dn = &dp.name; + } + + // Now substitute. + // + size_t i; + if (vn == "version") + { + return dc.string (); // Use normalized representation. + } + if (vn.compare (0, (i = 6), "check(") == 0 || + vn.compare (0, (i = 10), "condition(") == 0) + { + size_t j (vn.find_first_of (",)", i)); + + if (j == string::npos || (vn[j] == ',' && vn.back () != ')')) + fail (l) << "missing closing ')'"; + + string vm (vn, i, j - i); // VER macro. + string sm (vn[j] == ',' // SNAP macro. + ? string (vn, j + 1, vn.size () - j - 2) + : string ()); + + trim (vm); + trim (sm); + + auto cond = [&l, &dc, &vm, &sm] () -> string + { + auto& miv (dc.min_version); + auto& mav (dc.max_version); + + bool mio (dc.min_open); + bool mao (dc.max_open); + + if (sm.empty () && + ((miv && miv->snapshot ()) || + (mav && mav->snapshot ()))) + fail (l) << "snapshot macro required for " << dc.string (); + + auto cmp = [] (const string& m, const char* o, uint64_t v) + { + return m + o + to_string (v) + "ULL"; + }; + + // Note that version orders everything among pre-releases (that E + // being 0/1). So the snapshot comparison is only necessary "inside" + // the same pre-release. + // + auto max_cmp = [&vm, &sm, mao, &mav, &cmp] (bool p = false) + { + string r; + + if (mav->snapshot ()) + { + r += (p ? "(" : ""); + + r += cmp (vm, " < ", mav->version) + " || ("; + r += cmp (vm, " == ", mav->version) + " && "; + r += cmp (sm, (mao ? " < " : " <= "), mav->snapshot_sn) + ")"; + + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mao ? " < " : " <= "), mav->version); + + return r; + }; + + auto min_cmp = [&vm, &sm, mio, &miv, &cmp] (bool p = false) + { + string r; + + if (miv->snapshot ()) + { + r += (p ? "(" : ""); + + r += cmp (vm, " > ", miv->version) + " || ("; + r += cmp (vm, " == ", miv->version) + " && "; + r += cmp (sm, (mio ? " > " : " >= "), miv->snapshot_sn) + ")"; + + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mio ? " > " : " >= "), miv->version); + + return r; + }; + + // < / <= + // + if (!miv) + return max_cmp (); + + // > / >= + // + if (!mav) + return min_cmp (); + + // == + // + if (*miv == *mav) + { + string r (cmp (vm, " == ", miv->version)); + + if (miv->snapshot ()) + r += " && " + cmp (sm, " == ", miv->snapshot_sn); + + return r; + } + + // range + // + return min_cmp (true) + " && " + max_cmp (true); + }; + + if (vn[1] == 'o') // condition + return cond (); + + string r; + + // This is tricky: if the version header hasn't been generated yet, + // then the check will fail. Maybe a better solution is to disable + // diagnostics and ignore (some) errors during dependency extraction. + // + r += "#ifdef " + vm + "\n"; + r += "# if !(" + cond () + ")\n"; + r += "# error incompatible " + dn->string () + " version, "; + r += dn->string () + ' ' + dc.string () + " is required\n"; + r += "# endif\n"; + r += "#endif"; + + return r; + } + else + fail (l) << "unknown dependency substitution '" << vn << "'" << endf; + } + + // manifest_install_rule + // + bool manifest_install_rule:: + match (action a, target& t, const string&) const + { + // We only match project's manifest. + // + if (!t.is_a<manifest> () || t.name != "manifest") + return false; + + // Must be in project's src_root. + // + const scope& s (t.base_scope ()); + if (s.root_scope () != &s || s.src_path () != t.dir) + return false; + + return file_rule::match (a, t, ""); + } + + auto_rmfile manifest_install_rule:: + install_pre (const file& t, const install_dir&) const + { + const path& p (t.path ()); + + const scope& rs (t.root_scope ()); + const module& m (*rs.lookup_module<module> (module::name)); + + if (!m.rewritten) + return auto_rmfile (p, false /* active */); + + // Our options are to use path::temp_path() or to create a .t file in + // the out tree. Somehow the latter feels more appropriate (even though + // if we crash in between, we won't clean it up). + // + return fixup_manifest (p, rs.out_path () / "manifest.t", m.version); + } + } +} diff --git a/libbuild2/version/rule.hxx b/libbuild2/version/rule.hxx new file mode 100644 index 0000000..ce21aa4 --- /dev/null +++ b/libbuild2/version/rule.hxx @@ -0,0 +1,52 @@ +// file : libbuild2/version/rule.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_VERSION_RULE_HXX +#define LIBBUILD2_VERSION_RULE_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/install/rule.hxx> + +#include <libbuild2/in/rule.hxx> + +namespace build2 +{ + namespace version + { + // Preprocess an .in file that depends on manifest. + // + class in_rule: public in::rule + { + public: + in_rule (): rule ("version.in 2", "version.in") {} + + virtual bool + match (action, target&, const string&) const override; + + virtual string + lookup (const location&, + action, + const target&, + const string&) const override; + }; + + // Pre-process manifest before installation to patch in the version. + // + class manifest_install_rule: public install::file_rule + { + public: + manifest_install_rule () {} + + virtual bool + match (action, target&, const string&) const override; + + virtual auto_rmfile + install_pre (const file&, const install_dir&) const override; + }; + } +} + +#endif // LIBBUILD2_VERSION_RULE_HXX diff --git a/libbuild2/version/snapshot-git.cxx b/libbuild2/version/snapshot-git.cxx new file mode 100644 index 0000000..b7ca084 --- /dev/null +++ b/libbuild2/version/snapshot-git.cxx @@ -0,0 +1,175 @@ +// file : libbuild2/version/snapshot-git.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <ctime> // time_t + +#include <libbutl/sha1.mxx> + +#include <libbuild2/version/snapshot.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace version + { + snapshot + extract_snapshot_git (const dir_path& src_root) + { + snapshot r; + const char* d (src_root.string ().c_str ()); + + // First check whether the working directory is clean. There doesn't + // seem to be a way to do everything in a single invocation (the + // porcelain v2 gives us the commit id but not timestamp). + // + + // If git status --porcelain returns anything, then the working + // directory is not clean. + // + { + const char* args[] {"git", "-C", d, "status", "--porcelain", nullptr}; + r.committed = run<string> ( + 3 /* verbosity */, + args, + [](string& s, bool) {return move (s);}).empty (); + } + + // Now extract the commit id and date. One might think that would be + // easy... Commit id is a SHA1 hash of the commit object. And commit + // object looks like this: + // + // commit <len>\0 + // <data> + // + // Where <len> is the size of <data> and <data> is the output of: + // + // git cat-file commit HEAD + // + // There is also one annoying special case: new repository without any + // commits. In this case the above command will fail (with diagnostics + // and non-zero exit code) because there is no HEAD. Of course, it can + // also fail for other reason (like broken repository) which would be + // hard to distinguish. Note, however, that we just ran git status and + // it would have most likely failed if this were the case. So here we + // (reluctantly) assume that the only reason git cat-file fails is if + // there is no HEAD (that we equal with the "new repository" condition + // which is, strictly speaking, might not be the case either). So we + // suppress any diagnostics, and handle non-zero exit code. + // + string data; + + const char* args[] { + "git", "-C", d, "cat-file", "commit", "HEAD", nullptr}; + process pr (run_start (3 /* verbosity */, + args, + 0 /* stdin */, + -1 /* stdout */, + false /* error */)); + + string l; + try + { + ifdstream is (move (pr.in_ofd), ifdstream::badbit); + + while (!eof (getline (is, l))) + { + data += l; + data += '\n'; // We assume there is always a newline. + + if (r.sn == 0 && l.compare (0, 10, "committer ") == 0) + try + { + // The line format is: + // + // committer <noise> <timestamp> <timezone> + // + // For example: + // + // committer John Doe <john@example.org> 1493117819 +0200 + // + // The timestamp is in seconds since UNIX epoch. The timezone + // appears to be always numeric (+0000 for UTC). Note that + // timestamp appears to be already in UTC with timezone being just + // for information it seems. + // + size_t p1 (l.rfind (' ')); // Can't be npos. + + size_t p2 (l.rfind (' ', p1 - 1)); + if (p2 == string::npos) + throw invalid_argument ("missing timestamp"); + + string ts (l, p2 + 1, p1 - p2 - 1); + time_t t (static_cast<time_t> (stoull (ts))); + +#if 0 + string tz (l, p1 + 1); + + if (tz.size () != 5) + throw invalid_argument ("invalid timezone"); + + unsigned long h (stoul (string (tz, 1, 2))); + unsigned long m (stoul (string (tz, 3, 2))); + unsigned long s (h * 3600 + m * 60); + + // The timezone indicates where the timestamp was generated so to + // convert to UTC we need to invert the sign. + // + switch (tz[0]) + { + case '+': t -= s; break; + case '-': t += s; break; + default: throw invalid_argument ("invalid timezone sign"); + } +#endif + // Represent as YYYYMMDDhhmmss. + // + r.sn = stoull (to_string (system_clock::from_time_t (t), + "%Y%m%d%H%M%S", + false /* special */, + false /* local (already in UTC) */)); + } + catch (const invalid_argument& e) + { + fail << "unable to extract git commit date from '" << l << "': " + << e; + } + } + + is.close (); + } + catch (const io_error&) + { + // Presumably the child process failed. Let run_finish() deal with + // that. + } + + if (!run_finish (args, pr, false /* error */, l)) + { + // Presumably new repository without HEAD. Return uncommitted snapshot + // with UNIX epoch as timestamp. + // + r.sn = 19700101000000ULL; + r.committed = false; + return r; + } + + if (r.sn == 0) + fail << "unable to extract git commit id/date for " << src_root; + + if (r.committed) + { + sha1 cs; + cs.append ("commit " + to_string (data.size ())); // Includes '\0'. + cs.append (data.c_str (), data.size ()); + r.id.assign (cs.string (), 12); // 12-characters abbreviated commit id. + } + else + r.sn++; // Add a second. + + return r; + } + } +} diff --git a/libbuild2/version/snapshot.cxx b/libbuild2/version/snapshot.cxx new file mode 100644 index 0000000..46b37f3 --- /dev/null +++ b/libbuild2/version/snapshot.cxx @@ -0,0 +1,39 @@ +// file : libbuild2/version/snapshot.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/version/snapshot.hxx> + +#include <libbuild2/filesystem.hxx> + +using namespace std; + +namespace build2 +{ + namespace version + { + snapshot + extract_snapshot_git (const dir_path&); + + static const path git (".git"); + + snapshot + extract_snapshot (const scope& rs) + { + // Ignore errors when checking for existence since we may be iterating + // over directories past any reasonable project boundaries. + // + for (dir_path d (rs.src_path ()); !d.empty (); d = d.directory ()) + { + // .git can be either a directory or a file in case of a submodule. + // + if (butl::entry_exists (d / git, + true /* follow_symlinks */, + true /* ignore_errors */)) + return extract_snapshot_git (d); + } + + return snapshot (); + } + } +} diff --git a/libbuild2/version/snapshot.hxx b/libbuild2/version/snapshot.hxx new file mode 100644 index 0000000..86b6eab --- /dev/null +++ b/libbuild2/version/snapshot.hxx @@ -0,0 +1,34 @@ +// file : libbuild2/version/snapshot.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_VERSION_SNAPSHOT_HXX +#define LIBBUILD2_VERSION_SNAPSHOT_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/scope.hxx> + +namespace build2 +{ + namespace version + { + struct snapshot + { + uint64_t sn = 0; + string id; + bool committed = false; + + bool + empty () const {return sn == 0;} + }; + + // Return empty snapshot if unknown scm or uncommitted. + // + snapshot + extract_snapshot (const scope& rs); + } +} + +#endif // LIBBUILD2_VERSION_SNAPSHOT_HXX diff --git a/libbuild2/version/utility.cxx b/libbuild2/version/utility.cxx new file mode 100644 index 0000000..c93a251 --- /dev/null +++ b/libbuild2/version/utility.cxx @@ -0,0 +1,81 @@ +// file : libbuild2/version/utility.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/version/utility.hxx> + +#include <libbutl/manifest-parser.mxx> +#include <libbutl/manifest-serializer.mxx> + +#include <libbuild2/diagnostics.hxx> + +using namespace butl; + +namespace build2 +{ + namespace version + { + auto_rmfile + fixup_manifest (const path& in, path out, const standard_version& v) + { + auto_rmfile r (move (out), !dry_run /* active */); + + if (!dry_run) + { + try + { + permissions perm (path_permissions (in)); + + ifdstream ifs (in); + manifest_parser p (ifs, in.string ()); + + auto_fd ofd (fdopen (r.path, + fdopen_mode::out | + fdopen_mode::create | + fdopen_mode::exclusive | + fdopen_mode::binary, + perm)); + + ofdstream ofs (move (ofd)); + manifest_serializer s (ofs, r.path.string ()); + + manifest_name_value nv (p.next ()); + assert (nv.name.empty () && nv.value == "1"); // We just loaded it. + s.next (nv.name, nv.value); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + if (nv.name == "version") + nv.value = v.string (); + + s.next (nv.name, nv.value); + } + + s.next (nv.name, nv.value); // End of manifest. + s.next (nv.name, nv.value); // End of stream. + + ofs.close (); + ifs.close (); + } + catch (const manifest_parsing& e) + { + location l (&in, e.line, e.column); + fail (l) << e.description; + } + catch (const manifest_serialization& e) + { + location l (&r.path); + fail (l) << e.description; + } + catch (const io_error& e) + { + fail << "io error: " << e << + info << "while reading " << in << + info << "while writing " << r.path; + } + } + + return r; + } + } +} diff --git a/libbuild2/version/utility.hxx b/libbuild2/version/utility.hxx new file mode 100644 index 0000000..16e8c78 --- /dev/null +++ b/libbuild2/version/utility.hxx @@ -0,0 +1,25 @@ +// file : libbuild2/version/utility.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_VERSION_UTILITY_HXX +#define LIBBUILD2_VERSION_UTILITY_HXX + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/filesystem.hxx> + +namespace build2 +{ + namespace version + { + // Re-serialize the manifest fixing up the version. Note that this will + // not preserve comments. Probably acceptable for snapshots. + // + auto_rmfile + fixup_manifest (const path& in, path out, const standard_version&); + } +} + +#endif // LIBBUILD2_VERSION_UTILITY_HXX |