aboutsummaryrefslogtreecommitdiff
path: root/build2
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2017-04-26 15:52:15 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2017-04-26 15:53:23 +0200
commit8276cb927bafd338be237adbecf437e70042da99 (patch)
tree49218b58e1f50a65b58674177e6047f1ac9f6831 /build2
parent2fd9d3f177429b20797897360931badedbb0f0ef (diff)
Implement version module
Diffstat (limited to 'build2')
-rw-r--r--build2/b.cxx2
-rw-r--r--build2/buildfile6
-rw-r--r--build2/dist/init.cxx54
-rw-r--r--build2/dist/module65
-rw-r--r--build2/dist/module.cxx15
-rw-r--r--build2/dist/operation.cxx44
-rw-r--r--build2/utility39
-rw-r--r--build2/utility.txx13
-rw-r--r--build2/version/init31
-rw-r--r--build2/version/init.cxx295
-rw-r--r--build2/version/module32
-rw-r--r--build2/version/module.cxx15
-rw-r--r--build2/version/rule36
-rw-r--r--build2/version/rule.cxx151
-rw-r--r--build2/version/snapshot33
-rw-r--r--build2/version/snapshot-git.cxx106
-rw-r--r--build2/version/snapshot.cxx31
17 files changed, 917 insertions, 51 deletions
diff --git a/build2/b.cxx b/build2/b.cxx
index 47541c4..23d0e01 100644
--- a/build2/b.cxx
+++ b/build2/b.cxx
@@ -49,6 +49,7 @@ using namespace std;
#include <build2/test/init>
#include <build2/install/init>
#include <build2/pkgconfig/init>
+#include <build2/version/init>
namespace build2
{
@@ -292,6 +293,7 @@ main (int argc, char* argv[])
bm["dist"] = mf {&dist::boot, &dist::init};
bm["test"] = mf {&test::boot, &test::init};
bm["install"] = mf {&install::boot, &install::init};
+ bm["version"] = mf {&version::boot, &version::init};
bm["bin.vars"] = mf {nullptr, &bin::vars_init};
bm["bin.config"] = mf {nullptr, &bin::config_init};
diff --git a/build2/buildfile b/build2/buildfile
index 83f0a08..23511b2 100644
--- a/build2/buildfile
+++ b/build2/buildfile
@@ -73,6 +73,7 @@ exe{b}: \
cxx/{hxx cxx}{ init } \
cxx/{hxx cxx}{ target } \
dist/{hxx cxx}{ init } \
+ dist/{hxx cxx}{ module } \
dist/{hxx cxx}{ operation } \
dist/{hxx cxx}{ rule } \
pkgconfig/{hxx cxx}{ init } \
@@ -93,6 +94,11 @@ test/script/{hxx ixx cxx}{ regex } \
test/script/{hxx cxx}{ runner } \
test/script/{hxx ixx cxx}{ script } \
test/script/{hxx cxx}{ token } \
+ version/{hxx cxx}{ init } \
+ version/{hxx cxx}{ module } \
+ version/{hxx cxx}{ rule } \
+ version/{hxx cxx}{ snapshot } \
+ version/{ cxx}{ snapshot-git } \
liba{b} $libs
#\
diff --git a/build2/dist/init.cxx b/build2/dist/init.cxx
index be7b381..41927cd 100644
--- a/build2/dist/init.cxx
+++ b/build2/dist/init.cxx
@@ -11,6 +11,7 @@
#include <build2/config/utility>
#include <build2/dist/rule>
+#include <build2/dist/module>
#include <build2/dist/operation>
using namespace std;
@@ -23,7 +24,7 @@ namespace build2
static const rule rule_;
void
- boot (scope& rs, const location&, unique_ptr<module_base>&)
+ boot (scope& rs, const location&, unique_ptr<module_base>& mod)
{
tracer trace ("dist::boot");
@@ -36,30 +37,33 @@ namespace build2
// Enter module variables. Do it during boot in case they get assigned
// in bootstrap.build (which is customary for, e.g., dist.package).
//
- {
- auto& v (var_pool.rw (rs));
-
- // Note: some overridable, some not.
- //
- // config.dist.archives is a list of archive extensions that can be
- // optionally prefixed with a directory. If it is relative, then it is
- // prefixed with config.dist.root. Otherwise, the archive is written
- // to the absolute location.
- //
- v.insert<abs_dir_path> ("config.dist.root", true);
- v.insert<paths> ("config.dist.archives", true);
- v.insert<path> ("config.dist.cmd", true);
-
- v.insert<dir_path> ("dist.root");
- v.insert<process_path> ("dist.cmd");
- v.insert<paths> ("dist.archives");
-
- v.insert<bool> ("dist", variable_visibility::target); // Flag.
-
- // Project's package name.
- //
- v.insert<string> ("dist.package", variable_visibility::project);
- }
+ auto& vp (var_pool.rw (rs));
+
+ // Note: some overridable, some not.
+ //
+ // config.dist.archives is a list of archive extensions that can be
+ // optionally prefixed with a directory. If it is relative, then it is
+ // prefixed with config.dist.root. Otherwise, the archive is written
+ // to the absolute location.
+ //
+ vp.insert<abs_dir_path> ("config.dist.root", true);
+ vp.insert<paths> ("config.dist.archives", true);
+ vp.insert<path> ("config.dist.cmd", true);
+
+ vp.insert<dir_path> ("dist.root");
+ vp.insert<process_path> ("dist.cmd");
+ vp.insert<paths> ("dist.archives");
+
+ vp.insert<bool> ("dist", variable_visibility::target); // Flag.
+
+ // Project's package name.
+ //
+ auto& v_d_p (
+ vp.insert<string> ("dist.package", variable_visibility::project));
+
+ // Create the module.
+ //
+ mod.reset (new module (v_d_p));
}
bool
diff --git a/build2/dist/module b/build2/dist/module
new file mode 100644
index 0000000..5510423
--- /dev/null
+++ b/build2/dist/module
@@ -0,0 +1,65 @@
+// file : build2/dist/module -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BUILD2_DIST_MODULE
+#define BUILD2_DIST_MODULE
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <build2/module>
+#include <build2/variable>
+
+namespace build2
+{
+ namespace dist
+ {
+ struct module: module_base
+ {
+ static const string name;
+
+ const variable& var_dist_package;
+
+ // Distribution post-processing callbacks.
+ //
+ // The last component in the pattern may contain shell wildcards. If the
+ // path contains a directory, then it is matched from the distribution
+ // root only. Otherwise, it is matched against all the files being
+ // distributed. For example:
+ //
+ // buildfile - every buildfile
+ // ./buildfile - root buildfile only
+ // tests/buildfile - tests/buildfile only
+ //
+ // The callback is called with the absolute path of the matching file
+ // after it has been copied to the distribution directory. The project's
+ // root scope and callback-specific data are passed along.
+ //
+ using callback_func = void (const path&, const scope&, void*);
+
+ void
+ register_callback (path pattern, callback_func* f, void* data)
+ {
+ callbacks_.push_back (callback {move (pattern), f, data});
+ }
+
+ // Implementation details.
+ //
+ module (const variable& v_d_p)
+ : var_dist_package (v_d_p) {}
+
+ public:
+ struct callback
+ {
+ const path pattern;
+ callback_func* function;
+ void* data;
+ };
+
+ vector<callback> callbacks_;
+ };
+ }
+}
+
+#endif // BUILD2_DIST_MODULE
diff --git a/build2/dist/module.cxx b/build2/dist/module.cxx
new file mode 100644
index 0000000..05ca1cb
--- /dev/null
+++ b/build2/dist/module.cxx
@@ -0,0 +1,15 @@
+// file : build2/dist/module.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <build2/dist/module>
+
+using namespace std;
+
+namespace build2
+{
+ namespace dist
+ {
+ const string module::name ("dist");
+ }
+}
diff --git a/build2/dist/operation.cxx b/build2/dist/operation.cxx
index 033748e..859225d 100644
--- a/build2/dist/operation.cxx
+++ b/build2/dist/operation.cxx
@@ -4,6 +4,8 @@
#include <build2/dist/operation>
+#include <butl/filesystem> // path_match()
+
#include <build2/file>
#include <build2/dump>
#include <build2/scope>
@@ -13,6 +15,8 @@
#include <build2/filesystem>
#include <build2/diagnostics>
+#include <build2/dist/module>
+
using namespace std;
using namespace butl;
@@ -27,7 +31,9 @@ namespace build2
// install <file> <dir>
//
- static void
+ // Return the destination file path.
+ //
+ static path
install (const process_path& cmd, const file&, const dir_path&);
// cd <root> && tar|zip ... <dir>/<pkg>.<ext> <pkg>
@@ -286,23 +292,49 @@ namespace build2
install (dist_cmd, td);
- // Copy over all the files.
+ // Copy over all the files. Apply post-processing callbacks.
//
+ module& mod (*rs->modules.lookup<module> (module::name));
+
for (const void* v: files)
{
const file& t (*static_cast<const file*> (v));
// Figure out where this file is inside the target directory.
//
+ bool src (t.dir.sub (src_root));
+
dir_path d (td);
- d /= t.dir.sub (src_root)
+ d /= src
? t.dir.leaf (src_root)
: t.dir.leaf (out_root);
if (!exists (d))
install (dist_cmd, d);
- install (dist_cmd, t, d);
+ path r (install (dist_cmd, t, d));
+
+ for (module::callback cb: mod.callbacks_)
+ {
+ const path& pat (cb.pattern);
+
+ // If we have a directory, then it should be relative to the project
+ // root.
+ //
+ if (!pat.simple ())
+ {
+ assert (pat.relative ());
+
+ dir_path d ((src ? src_root : out_root) / pat.directory ());
+ d.normalize ();
+
+ if (d != t.dir)
+ continue;
+ }
+
+ if (path_match (pat.leaf ().string (), t.path ().leaf ().string ()))
+ cb.function (r, *rs, cb.data);
+ }
}
// Archive if requested.
@@ -367,7 +399,7 @@ namespace build2
// install <file> <dir>
//
- static void
+ static path
install (const process_path& cmd, const file& t, const dir_path& d)
{
dir_path reld (relative (d));
@@ -417,6 +449,8 @@ namespace build2
throw failed ();
}
+
+ return d / relf.leaf ();
}
static void
diff --git a/build2/utility b/build2/utility
index 5e2fd22..2bcc52c 100644
--- a/build2/utility
+++ b/build2/utility
@@ -173,80 +173,83 @@ namespace build2
// is false and the program exits with the non-zero status, then an empty T
// instance is returned).
//
- // If checksum is not NULL, then feed it the content of each line.
+ // If checksum is not NULL, then feed it the content of each tripped line
+ // (including those that come after the callback returns non-empty object).
//
- template <typename T>
+ template <typename T, typename F>
T
run (const process_path&,
const char* args[],
- T (*) (string&),
+ F&&,
bool error = true,
bool ignore_exit = false,
sha256* checksum = nullptr);
- template <typename T>
+ template <typename T, typename F>
inline T
run (const char* args[],
- T (*f) (string&),
+ F&& f,
bool error = true,
bool ignore_exit = false,
sha256* checksum = nullptr)
{
- return run<T> (run_search (args[0]), args, f, error, ignore_exit, checksum);
+ return run<T> (
+ run_search (
+ args[0]), args, forward<F> (f), error, ignore_exit, checksum);
}
// run <prog>
//
- template <typename T>
+ template <typename T, typename F>
inline T
run (const path& prog,
- T (*f) (string&),
+ F&& f,
bool error = true,
bool ignore_exit = false,
sha256* checksum = nullptr)
{
const char* args[] = {prog.string ().c_str (), nullptr};
- return run<T> (args, f, error, ignore_exit, checksum);
+ return run<T> (args, forward<F> (f), error, ignore_exit, checksum);
}
- template <typename T>
+ template <typename T, typename F>
inline T
run (const process_path& pp,
- T (*f) (string&),
+ F&& f,
bool error = true,
bool ignore_exit = false,
sha256* checksum = nullptr)
{
const char* args[] = {pp.recall_string (), nullptr};
- return run<T> (pp, args, f, error, ignore_exit, checksum);
+ return run<T> (pp, args, forward<F> (f), error, ignore_exit, checksum);
}
// run <prog> <arg>
//
- template <typename T>
+ template <typename T, typename F>
inline T
run (const path& prog,
const char* arg,
- T (*f) (string&),
+ F&& f,
bool error = true,
bool ignore_exit = false,
sha256* checksum = nullptr)
{
const char* args[] = {prog.string ().c_str (), arg, nullptr};
- return run<T> (args, f, error, ignore_exit, checksum);
+ return run<T> (args, forward<F> (f), error, ignore_exit, checksum);
}
- template <typename T>
+ template <typename T, typename F>
inline T
run (const process_path& pp,
const char* arg,
- T (*f) (string&),
+ F&& f,
bool error = true,
bool ignore_exit = false,
sha256* checksum = nullptr)
{
const char* args[] = {pp.recall_string (), arg, nullptr};
- return run<T> (pp, args, f, error, ignore_exit, checksum);
+ return run<T> (pp, args, forward<F> (f), error, ignore_exit, checksum);
}
// Empty string and path.
diff --git a/build2/utility.txx b/build2/utility.txx
index 2cc6a33..28c7d6e 100644
--- a/build2/utility.txx
+++ b/build2/utility.txx
@@ -29,11 +29,11 @@ namespace build2
return p;
}
- template <typename T>
+ template <typename T, typename F>
T
run (const process_path& pp,
const char* args[],
- T (*f) (string&),
+ F&& f,
bool err,
bool ignore_exit,
sha256* checksum)
@@ -45,7 +45,7 @@ namespace build2
try
{
- ifdstream is (move (pr.in_ofd));
+ ifdstream is (move (pr.in_ofd), butl::fdstream_mode::skip);
while (is.peek () != ifdstream::traits_type::eof () && // Keep last line.
getline (is, l))
@@ -56,8 +56,15 @@ namespace build2
checksum->append (l);
if (r.empty ())
+ {
r = f (l);
+
+ if (!r.empty () && checksum == nullptr)
+ break;
+ }
}
+
+ is.close ();
}
catch (const io_error&)
{
diff --git a/build2/version/init b/build2/version/init
new file mode 100644
index 0000000..8ffb999
--- /dev/null
+++ b/build2/version/init
@@ -0,0 +1,31 @@
+// file : build2/version/init -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BUILD2_VERSION_INIT
+#define BUILD2_VERSION_INIT
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <build2/module>
+
+namespace build2
+{
+ namespace version
+ {
+ void
+ boot (scope&, const location&, unique_ptr<module_base>&);
+
+ bool
+ init (scope&,
+ scope&,
+ const location&,
+ unique_ptr<module_base>&,
+ bool,
+ bool,
+ const variable_map&);
+ }
+}
+
+#endif // BUILD2_VERSION_INIT
diff --git a/build2/version/init.cxx b/build2/version/init.cxx
new file mode 100644
index 0000000..947f4a8
--- /dev/null
+++ b/build2/version/init.cxx
@@ -0,0 +1,295 @@
+// file : build2/version/init.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <build2/version/init>
+
+#include <butl/manifest-parser>
+#include <butl/manifest-serializer>
+
+#include <build2/scope>
+#include <build2/context>
+#include <build2/variable>
+#include <build2/diagnostics>
+
+#include <build2/dist/module>
+
+#include <build2/version/rule>
+#include <build2/version/module>
+#include <build2/version/snapshot>
+
+using namespace std;
+using namespace butl;
+
+namespace build2
+{
+ namespace version
+ {
+ static const path manifest ("manifest");
+
+ static const version_doc version_doc_;
+
+ void
+ boot (scope& rs, const location& l, unique_ptr<module_base>& mod)
+ {
+ tracer trace ("version::boot");
+ l5 ([&]{trace << "for " << rs.out_path ();});
+
+ // Extract the version from the manifest file.
+ //
+ standard_version v;
+ {
+ path f (rs.src_path () / manifest);
+
+ 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 == "version")
+ {
+ try
+ {
+ v = standard_version (nv.value);
+ }
+ catch (const invalid_argument& e)
+ {
+ fail << "invalid standard version '" << nv.value << "': " << e;
+ }
+ break;
+ }
+ }
+ }
+ 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 sn and id (e.g., commit date and id from git). If there is
+ // uncommitted stuff, then leave it as .z.
+ //
+ bool patched (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);
+ patched = true;
+ }
+ }
+
+ // 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);
+ };
+
+ // Enough of project version for unique identification (can be used in
+ // places like soname, etc).
+ //
+ string id (v.string_version ());
+ if (v.snapshot ()) // Trailing dot already in id.
+ {
+ id += (v.snapshot_sn == standard_version::latest_sn
+ ? "z"
+ : (v.snapshot_id.empty ()
+ ? to_string (v.snapshot_sn):
+ v.snapshot_id));
+ }
+
+ set ("version", v.string ()); // Package version.
+
+ set ("version.project", v.string_project ());
+ set ("version.project_number", v.version);
+ set ("version.project_id", move (id));
+
+ 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 ()));
+
+ set ("version.alpha", v.alpha ()); // bool
+ set ("version.beta", v.beta ()); // bool
+ set ("version.pre_release", v.alpha () || v.beta ());
+ set ("version.pre_release_string", v.string_pre_release ());
+ set ("version.pre_release_number", uint64_t (v.pre_release ()));
+
+ 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.revision", uint64_t (v.revision));
+
+ // Create the module.
+ //
+ mod.reset (new module (move (v), patched));
+ }
+
+ 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";
+
+ module& m (static_cast<module&> (*mod));
+ const standard_version& v (m.version);
+
+ // If the dist module has been loaded, set its dist.package and register
+ // the post-processing callback.
+ //
+ if (auto* dm = rs.modules.lookup<dist::module> (dist::module::name))
+ {
+ // Don't touch if dist.package was set by the user.
+ //
+ value& val (rs.assign (dm->var_dist_package));
+
+ if (!val)
+ {
+ string p (cast<string> (rs.vars[var_project]));
+ p += '-';
+ p += v.string ();
+ val = move (p);
+
+ // Only register the post-processing callback if this a snapshot.
+ //
+ if (v.snapshot ())
+ dm->register_callback (dir_path (".") / manifest,
+ &dist_callback,
+ &m);
+ }
+ }
+
+ // Register rules.
+ //
+ {
+ auto& r (rs.rules);
+
+ r.insert<doc> (perform_update_id, "version.doc", version_doc_);
+ r.insert<doc> (perform_clean_id, "version.doc", version_doc_);
+ r.insert<doc> (configure_update_id, "version.doc", version_doc_);
+ }
+
+ return true;
+ }
+
+ static void
+ dist_callback (const path& f, const scope& rs, void* data)
+ {
+ module& m (*static_cast<module*> (data));
+ const standard_version v (m.version);
+
+ // Complain if this is an uncommitted snapshot.
+ //
+ if (v.snapshot_sn == standard_version::latest_sn)
+ fail << "distribution of uncommitted project " << rs.src_path ();
+
+ // The plan is simple, re-serialize the manifest into a temporary file
+ // fixing up the version. Then move the temporary file to the original.
+ //
+ path t;
+ try
+ {
+ permissions perm (path_permissions (f));
+
+ ifdstream ifs (f);
+ manifest_parser p (ifs, f.string ());
+
+ t = path::temp_path ("manifest");
+ auto_fd ofd (fdopen (t,
+ fdopen_mode::out |
+ fdopen_mode::create |
+ fdopen_mode::exclusive |
+ fdopen_mode::binary,
+ perm));
+ auto_rmfile arm (t); // Try to remove on failure ignoring errors.
+
+ ofdstream ofs (move (ofd));
+ manifest_serializer s (ofs, t.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 ();
+
+ mvfile (t, f, (cpflags::overwrite_content |
+ cpflags::overwrite_permissions));
+ arm.cancel ();
+ }
+ catch (const manifest_parsing& e)
+ {
+ location l (&f, e.line, e.column);
+ fail (l) << e.description;
+ }
+ catch (const manifest_serialization& e)
+ {
+ location l (&t);
+ fail (l) << e.description;
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to overwrite " << f << ": " << e;
+ }
+ catch (const system_error& e) // EACCES, etc.
+ {
+ fail << "unable to overwrite " << f << ": " << e;
+ }
+ }
+ }
+}
diff --git a/build2/version/module b/build2/version/module
new file mode 100644
index 0000000..d5c1f01
--- /dev/null
+++ b/build2/version/module
@@ -0,0 +1,32 @@
+// file : build2/version/module -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BUILD2_VERSION_MODULE
+#define BUILD2_VERSION_MODULE
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <butl/standard-version>
+
+#include <build2/module>
+
+namespace build2
+{
+ namespace version
+ {
+ struct module: module_base
+ {
+ static const string name;
+
+ butl::standard_version version;
+ bool version_patched; // True if snapshot was patched in.
+
+ module (butl::standard_version v, bool vp)
+ : version (move (v)), version_patched (vp) {}
+ };
+ }
+}
+
+#endif // BUILD2_VERSION_MODULE
diff --git a/build2/version/module.cxx b/build2/version/module.cxx
new file mode 100644
index 0000000..9865271
--- /dev/null
+++ b/build2/version/module.cxx
@@ -0,0 +1,15 @@
+// file : build2/version/module.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <build2/version/module>
+
+using namespace std;
+
+namespace build2
+{
+ namespace version
+ {
+ const string module::name ("version");
+ }
+}
diff --git a/build2/version/rule b/build2/version/rule
new file mode 100644
index 0000000..75a8c12
--- /dev/null
+++ b/build2/version/rule
@@ -0,0 +1,36 @@
+// file : build2/version/rule -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BUILD2_VERSION_RULE
+#define BUILD2_VERSION_RULE
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <build2/rule>
+
+namespace build2
+{
+ namespace version
+ {
+ // Generate a version file.
+ //
+ class version_doc: public rule
+ {
+ public:
+ version_doc () {}
+
+ virtual match_result
+ match (action, target&, const string&) const override;
+
+ virtual recipe
+ apply (action, target&) const override;
+
+ static target_state
+ perform_update (action, const target&);
+ };
+ }
+}
+
+#endif // BUILD2_VERSION_RULE
diff --git a/build2/version/rule.cxx b/build2/version/rule.cxx
new file mode 100644
index 0000000..1da37bb
--- /dev/null
+++ b/build2/version/rule.cxx
@@ -0,0 +1,151 @@
+// file : build2/version/rule.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <build2/version/rule>
+
+#include <build2/scope>
+#include <build2/target>
+#include <build2/context>
+#include <build2/algorithm>
+#include <build2/filesystem>
+#include <build2/diagnostics>
+
+#include <build2/version/module>
+
+using namespace std;
+using namespace butl;
+
+namespace build2
+{
+ namespace version
+ {
+ match_result version_doc::
+ match (action a, target& xt, const string&) const
+ {
+ tracer trace ("version::version_file::match");
+
+ doc& t (static_cast<doc&> (xt));
+
+ // We match any doc{} target that is called version (potentially with
+ // some extension and different case) and that has a dependency on a
+ // file called manifest from the same project's src_root.
+ //
+ if (casecmp (t.name, "version") != 0)
+ {
+ l4 ([&]{trace << "name mismatch for target " << t;});
+ return false;
+ }
+
+ const scope& rs (t.root_scope ());
+
+ for (prerequisite_member p: group_prerequisite_members (a, t))
+ {
+ if (!p.is_a<file> ())
+ continue;
+
+ const target& pt (p.search ());
+
+ if (pt.name != "manifest")
+ continue;
+
+ const scope* prs (pt.base_scope ().root_scope ());
+
+ if (prs == nullptr || prs != &rs || pt.dir != rs.src_path ())
+ continue;
+
+ return true;
+ }
+
+ l4 ([&]{trace << "no manifest prerequisite for target " << t;});
+ return false;
+ }
+
+ recipe version_doc::
+ apply (action a, target& xt) const
+ {
+ doc& t (static_cast<doc&> (xt));
+
+ // Derive file names for the members.
+ //
+ t.derive_path ();
+
+ // Inject dependency on the output directory.
+ //
+ inject_fsdir (a, t);
+
+ // Match prerequisite members.
+ //
+ match_prerequisite_members (a, t);
+
+ switch (a)
+ {
+ case perform_update_id: return &perform_update;
+ case perform_clean_id: return &perform_clean; // Standard clean.
+ default: return noop_recipe; // Configure update.
+ }
+ }
+
+ target_state version_doc::
+ perform_update (action a, const target& xt)
+ {
+ const doc& t (xt.as<const doc&> ());
+ const path& f (t.path ());
+
+ const scope& rs (t.root_scope ());
+ const module& m (*rs.modules.lookup<module> (module::name));
+
+ // Determine if anything needs to be updated.
+ //
+ // While we use the manifest file to decide whether we need to
+ // regenerate the version file, the version itself we get from the
+ // module (we checked above that manifest and version files are in the
+ // same project).
+ //
+ // That is, unless we patched the snapshot information in, in which case
+ // we have to compare the contents.
+ //
+ {
+ auto p (execute_prerequisites (a, t, t.load_mtime ()));
+
+ if (!p.first)
+ {
+ if (!m.version_patched || !exists (f))
+ return p.second;
+
+ try
+ {
+ ifdstream ifs (f, fdopen_mode::in, ifdstream::badbit);
+
+ string s;
+ getline (ifs, s);
+
+ if (s == m.version.string_project ())
+ return p.second;
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to read " << f << ": " << e;
+ }
+ }
+ }
+
+ if (verb >= 2)
+ text << "cat >" << f;
+
+ try
+ {
+ ofdstream ofs (f);
+ ofs << m.version.string_project () << endl;
+ ofs.close ();
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to write " << f << ": " << e;
+ }
+
+ t.mtime (system_clock::now ());
+ return target_state::changed;
+ }
+ }
+}
diff --git a/build2/version/snapshot b/build2/version/snapshot
new file mode 100644
index 0000000..a279729
--- /dev/null
+++ b/build2/version/snapshot
@@ -0,0 +1,33 @@
+// file : build2/version/snapshot -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BUILD2_VERSION_SNAPSHOT
+#define BUILD2_VERSION_SNAPSHOT
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <build2/scope>
+
+namespace build2
+{
+ namespace version
+ {
+ struct snapshot
+ {
+ uint64_t sn = 0;
+ string id;
+
+ bool
+ empty () const {return sn == 0;}
+ };
+
+ // Return empty snapshot if unknown scm or uncommitted.
+ //
+ snapshot
+ extract_snapshot (const scope& rs);
+ }
+}
+
+#endif // BUILD2_VERSION_SNAPSHOT
diff --git a/build2/version/snapshot-git.cxx b/build2/version/snapshot-git.cxx
new file mode 100644
index 0000000..b5db470
--- /dev/null
+++ b/build2/version/snapshot-git.cxx
@@ -0,0 +1,106 @@
+// file : build2/version/snapshot-git.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <build2/version/snapshot>
+
+using namespace std;
+
+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};
+
+ if (!run<string> (args, [] (string& s) {return move (s);}).empty ())
+ return r;
+ }
+
+ // Now extract the commit id and date.
+ //
+ auto extract = [&r] (string& s) -> snapshot
+ {
+ if (s.compare (0, 5, "tree ") == 0)
+ {
+ // The 16-characters abbreviated commit id.
+ //
+ r.id.assign (s, 5, 16);
+
+ if (r.id.size () != 16)
+ fail << "unable to extract git commit id from '" << s << "'";
+ }
+ else if (s.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).
+ //
+ size_t p1 (s.rfind (' ')); // Can't be npos.
+ string tz (s, p1 + 1);
+
+ size_t p2 (s.rfind (' ', p1 - 1));
+ if (p2 == string::npos)
+ throw invalid_argument ("missing timestamp");
+
+ string ts (s, p2 + 1, p1 - p2 - 1);
+ r.sn = stoull (ts);
+
+ 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 '+': r.sn -= s; break;
+ case '-': r.sn += s; break;
+ default: throw invalid_argument ("invalid timezone sign");
+ }
+ }
+ catch (const invalid_argument& e)
+ {
+ fail << "unable to extract git commit date from '" << s << "': " << e;
+ }
+
+ return (r.id.empty () || r.sn == 0) ? snapshot () : move (r);
+ };
+
+ const char* args[] {
+ "git", "-C", d, "cat-file", "commit", "HEAD", nullptr};
+ r = run<snapshot> (args, extract);
+
+ if (r.empty ())
+ fail << "unable to extract git commit id/date for " << src_root;
+
+ return r;
+ }
+ }
+}
diff --git a/build2/version/snapshot.cxx b/build2/version/snapshot.cxx
new file mode 100644
index 0000000..be6c147
--- /dev/null
+++ b/build2/version/snapshot.cxx
@@ -0,0 +1,31 @@
+// file : build2/version/snapshot.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <build2/version/snapshot>
+
+#include <build2/filesystem>
+
+using namespace std;
+
+namespace build2
+{
+ namespace version
+ {
+ snapshot
+ extract_snapshot_git (const dir_path&);
+
+ static const dir_path git (".git");
+
+ snapshot
+ extract_snapshot (const scope& rs)
+ {
+ const dir_path& src_root (rs.src_path ());
+
+ if (exists (src_root / git))
+ return extract_snapshot_git (src_root);
+
+ return snapshot ();
+ }
+ }
+}