From 8276cb927bafd338be237adbecf437e70042da99 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 26 Apr 2017 15:52:15 +0200 Subject: Implement version module --- build2/b.cxx | 2 + build2/buildfile | 6 + build2/dist/init.cxx | 54 ++++---- build2/dist/module | 65 +++++++++ build2/dist/module.cxx | 15 ++ build2/dist/operation.cxx | 44 +++++- build2/utility | 39 +++--- build2/utility.txx | 13 +- build2/version/init | 31 +++++ build2/version/init.cxx | 295 ++++++++++++++++++++++++++++++++++++++++ build2/version/module | 32 +++++ build2/version/module.cxx | 15 ++ build2/version/rule | 36 +++++ build2/version/rule.cxx | 151 ++++++++++++++++++++ build2/version/snapshot | 33 +++++ build2/version/snapshot-git.cxx | 106 +++++++++++++++ build2/version/snapshot.cxx | 31 +++++ 17 files changed, 917 insertions(+), 51 deletions(-) create mode 100644 build2/dist/module create mode 100644 build2/dist/module.cxx create mode 100644 build2/version/init create mode 100644 build2/version/init.cxx create mode 100644 build2/version/module create mode 100644 build2/version/module.cxx create mode 100644 build2/version/rule create mode 100644 build2/version/rule.cxx create mode 100644 build2/version/snapshot create mode 100644 build2/version/snapshot-git.cxx create mode 100644 build2/version/snapshot.cxx (limited to 'build2') 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 #include #include +#include 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 #include +#include #include using namespace std; @@ -23,7 +24,7 @@ namespace build2 static const rule rule_; void - boot (scope& rs, const location&, unique_ptr&) + boot (scope& rs, const location&, unique_ptr& 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 ("config.dist.root", true); - v.insert ("config.dist.archives", true); - v.insert ("config.dist.cmd", true); - - v.insert ("dist.root"); - v.insert ("dist.cmd"); - v.insert ("dist.archives"); - - v.insert ("dist", variable_visibility::target); // Flag. - - // Project's package name. - // - v.insert ("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 ("config.dist.root", true); + vp.insert ("config.dist.archives", true); + vp.insert ("config.dist.cmd", true); + + vp.insert ("dist.root"); + vp.insert ("dist.cmd"); + vp.insert ("dist.archives"); + + vp.insert ("dist", variable_visibility::target); // Flag. + + // Project's package name. + // + auto& v_d_p ( + vp.insert ("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 +#include + +#include +#include + +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 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 + +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 +#include // path_match() + #include #include #include @@ -13,6 +15,8 @@ #include #include +#include + using namespace std; using namespace butl; @@ -27,7 +31,9 @@ namespace build2 // install // - static void + // Return the destination file path. + // + static path install (const process_path& cmd, const file&, const dir_path&); // cd && tar|zip ... /. @@ -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::name)); + for (const void* v: files) { const file& t (*static_cast (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 // - 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 + template T run (const process_path&, const char* args[], - T (*) (string&), + F&&, bool error = true, bool ignore_exit = false, sha256* checksum = nullptr); - template + template inline T run (const char* args[], - T (*f) (string&), + F&& f, bool error = true, bool ignore_exit = false, sha256* checksum = nullptr) { - return run (run_search (args[0]), args, f, error, ignore_exit, checksum); + return run ( + run_search ( + args[0]), args, forward (f), error, ignore_exit, checksum); } // run // - template + template 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 (args, f, error, ignore_exit, checksum); + return run (args, forward (f), error, ignore_exit, checksum); } - template + template 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 (pp, args, f, error, ignore_exit, checksum); + return run (pp, args, forward (f), error, ignore_exit, checksum); } // run // - template + template 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 (args, f, error, ignore_exit, checksum); + return run (args, forward (f), error, ignore_exit, checksum); } - template + template 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 (pp, args, f, error, ignore_exit, checksum); + return run (pp, args, forward (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 + template 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 +#include + +#include + +namespace build2 +{ + namespace version + { + void + boot (scope&, const location&, unique_ptr&); + + bool + init (scope&, + scope&, + const location&, + unique_ptr&, + 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 + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +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& 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 (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& mod, + bool first, + bool, + const variable_map&) + { + tracer trace ("version::init"); + + if (!first) + fail (l) << "multiple version module initializations"; + + module& m (static_cast (*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::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 (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 (perform_update_id, "version.doc", version_doc_); + r.insert (perform_clean_id, "version.doc", version_doc_); + r.insert (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 (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 +#include + +#include + +#include + +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 + +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 +#include + +#include + +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 + +#include +#include +#include +#include +#include +#include + +#include + +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 (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 ()) + 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 (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 path& f (t.path ()); + + const scope& rs (t.root_scope ()); + const module& m (*rs.modules.lookup (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 +#include + +#include + +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 + +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 (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 + // + // For example: + // + // committer John Doe 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 (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 + +#include + +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 (); + } + } +} -- cgit v1.1