aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/version
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2019-08-01 16:10:48 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2019-08-01 16:41:08 +0300
commitda9cbf29c403d27c2940f9b31199c4648f8ae4a1 (patch)
tree9732de6f48bdaead2becf32be41b1284e1e83e00 /libbuild2/version
parent8e69e09b7ec68377758c63092f9b254e95a0d7be (diff)
Move version build system module to separate library
Diffstat (limited to 'libbuild2/version')
-rw-r--r--libbuild2/version/buildfile66
-rw-r--r--libbuild2/version/export.hxx34
-rw-r--r--libbuild2/version/init.cxx407
-rw-r--r--libbuild2/version/init.hxx28
-rw-r--r--libbuild2/version/module.cxx15
-rw-r--r--libbuild2/version/module.hxx64
-rw-r--r--libbuild2/version/rule.cxx334
-rw-r--r--libbuild2/version/rule.hxx52
-rw-r--r--libbuild2/version/snapshot-git.cxx175
-rw-r--r--libbuild2/version/snapshot.cxx39
-rw-r--r--libbuild2/version/snapshot.hxx34
-rw-r--r--libbuild2/version/utility.cxx81
-rw-r--r--libbuild2/version/utility.hxx25
13 files changed, 1354 insertions, 0 deletions
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