diff options
author | Boris Kolpackov <boris@codesynthesis.com> | 2017-04-26 15:52:15 +0200 |
---|---|---|
committer | Boris Kolpackov <boris@codesynthesis.com> | 2017-04-26 15:53:23 +0200 |
commit | 8276cb927bafd338be237adbecf437e70042da99 (patch) | |
tree | 49218b58e1f50a65b58674177e6047f1ac9f6831 | |
parent | 2fd9d3f177429b20797897360931badedbb0f0ef (diff) |
Implement version module
-rw-r--r-- | build2/b.cxx | 2 | ||||
-rw-r--r-- | build2/buildfile | 6 | ||||
-rw-r--r-- | build2/dist/init.cxx | 54 | ||||
-rw-r--r-- | build2/dist/module | 65 | ||||
-rw-r--r-- | build2/dist/module.cxx | 15 | ||||
-rw-r--r-- | build2/dist/operation.cxx | 44 | ||||
-rw-r--r-- | build2/utility | 39 | ||||
-rw-r--r-- | build2/utility.txx | 13 | ||||
-rw-r--r-- | build2/version/init | 31 | ||||
-rw-r--r-- | build2/version/init.cxx | 295 | ||||
-rw-r--r-- | build2/version/module | 32 | ||||
-rw-r--r-- | build2/version/module.cxx | 15 | ||||
-rw-r--r-- | build2/version/rule | 36 | ||||
-rw-r--r-- | build2/version/rule.cxx | 151 | ||||
-rw-r--r-- | build2/version/snapshot | 33 | ||||
-rw-r--r-- | build2/version/snapshot-git.cxx | 106 | ||||
-rw-r--r-- | build2/version/snapshot.cxx | 31 | ||||
-rw-r--r-- | doc/manual.cli | 116 |
18 files changed, 1004 insertions, 80 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 (); + } + } +} diff --git a/doc/manual.cli b/doc/manual.cli index 7f27ff8..b514055 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -251,14 +251,14 @@ special treatment can be inhibited by specifying the target type explicitly \h1#module-version|Version Module| A project can use any version format as long as it meets the package version -requirements. The \c{build2} toolchain also provides additional functionality -for managing projects that conform to the \i{standard version} format. If you -are starting a new project that uses \c{build2}, you are strongly encouraged -to use this versioning scheme since it is based on much thought and -experience. If you decide not to follow this advice, you are essentially on -your own when version management is concerned. - -The \c{build2} standard project version conforms to \l{http://semver.org +requirements. The toolchain also provides additional functionality for +managing projects that conform to the \c{build2} \i{standard version} +format. If you are starting a new project that uses \c{build2}, you are +strongly encouraged to use this versioning scheme. It is based on much thought +and, often painful, experience. If you decide not to follow this advice, you +are essentially on your own when version management is concerned. + +The standard \c{build2} project version conforms to \l{http://semver.org Semantic Versioning} and has the following form: \ @@ -301,7 +301,7 @@ While the binary compatibility must be set in stone, the source compatibility rules can sometimes be bent. For example, you may decide to make a breaking change in a rarely used interface as part of a minor release. Note also that in the context of C++ deciding whether a change is binary-compatible is a -non-trivial task. There are resources that list the rules but no automatic +non-trivial task. There are resources that list the rules but no automated tooling yet. If unsure, increment \i{minor}. If present, the \i{prerel} component signifies a pre-release. Two types of @@ -327,13 +327,13 @@ Note that there is no support for release candidates. Instead, it is recommended that you use later-stage beta releases for this purpose (and, if you wish, call them \"release candidates\" in announcements, etc). -What version should we use during development? The common approach is to +What version should be used during development? The common approach is to increment to the next version and use that until the release. This has one major drawback: if we publish intermediate snapshots (for example, for testing) they will all be indistinguishable both between each other and, even worse, from the final release. One way to remedy this is to increment the -pre-release number before each publications. However, unless automated, this -will be burdensome and error prone. Also, there is a real possibility of +pre-release number before each publication. However, unless automated, this +will be burdensome and error-prone. Also, there is a real possibility of running out of version numbers if, for example, we do continuous integration by testing (and therefore publishing) each commit. @@ -355,23 +355,24 @@ before the next (\c{a.1} and, perhaps, \c{a.2} in the above example) and is uniquely identified by the snapshot sequence number (\i{snapsn}) and snapshot id (\i{snapid}). -The \i{num} component have the same semantics as in the final pre-releases -except that it can be 0. The \i{snapsn} component should be either the -special value '\c{z}' or a numeric, non-zero value that increases for -each subsequent snapshot. The \i{snapid} component, if present, should -be an alpha-numeric value that uniquely identifies the snapshot. It is -not required for version comparison (\i{snapsn} should be sufficient) -and is included for reference. +The \i{num} component has the same semantics as in the final pre-releases +except that it can be 0. The \i{snapsn} component should be either the special +value '\c{z}' or a numeric, non-zero value that increases for each subsequent +snapshot. It must fit into an unsigned 64-bit integer. The \i{snapid} +component, if present, should be an alpha-numeric value that uniquely +identifies the snapshot. It is not required for version comparison (\i{snapsn} +should be sufficient) and is included for reference. It must not be longer +than 16 characters. Where do the snapshot sn and id come from? Normally from the version control system. For example, for \c{git}, \i{snapsn} is the commit date (as UNIX -timestamp) and \i{snapid} is a 16-character abbreviated commit id. As -discussed below, the \c{build2} \c{version} module extracts all this -data automatically. +timestamp in the UTC timezone) and \i{snapid} is a 16-character abbreviated +commit id. As discussed below, the \c{build2} \c{version} module extracts +and manages all this information automatically. -The special '\c{z}' \i{snapsn} value identifies a latest or uncommitted -snapshot. It is chosen to be greater than any other possible \i{snapsn} -value and its use is discussed below. +The special '\c{z}' \i{snapsn} value identifies the \i{latest} or +\i{uncommitted} snapshot. It is chosen to be greater than any other possible +\i{snapsn} value and its use is discussed further below. As an illustration of this approach, let's examine how versions change during the lifetime of a project: @@ -398,11 +399,11 @@ even from alpha to release). We cannot, however, jump backwards (for example, from beta back to alpha). As a result, a sensible strategy is to start with \c{a.0} since it can always be upgraded (but not downgrade) at a later stage. -In terms of the version control system, the recommended workflow is as +When it comes to the version control systems, the recommended workflow is as follows: The change to the final version should be the last commit in the -(pre-)release. It is also a good idea to tag this commit with the version. A -commit immediately after that should change the version to snapshot -essentially \"opening\" the repository for development. +(pre-)release. It is also a good idea to tag this commit with the project +version. A commit immediately after that should change the version to a +snapshot essentially \"opening\" the repository for development. The project version without the snapshot part can be represented as a 64-bit decimal value comparable as integers (for example, in preprocessor @@ -434,4 +435,61 @@ For example: 3.0.0-b.2 0029999995020 2.2.0-a.1.z 0020019990011 \ + +A project that uses standard versioning can rely on the \c{build2} \c{version} +module to simplify and automate version managements. The \c{version} module +has two primary functions: eliminate the need to change the version anywhere +except in the project's manifest file and automatically extract and propagate +the snapshot information (serial number and id). + +The \c{version} module must be loaded in the project's \c{bootstrap.build}. +While being loaded, it reads the project's manifest and extracts its version +(which must be in the standard form). The version is then parsed and presented +as the following build system variables (which can be used in the buildfiles): + +\ +[string] version # 2~1.2.3-b.4.1234567.deadbeef+3 + +[string] version.project # 1.2.3-b.4.1234567.deadbeef +[uint64] version.project_number # 0010020025041 +[string] version.project_id # 1.2.3-b.4.deadbeef + +[uint64] version.epoch # 2 + +[uint64] version.major # 1 +[uint64] version.minor # 2 +[uint64] version.patch # 3 + +[bool] version.alpha # false +[bool] version.beta # true +[bool] version.pre_release # true +[string] version.pre_release_string # b.4 +[uint64] version.pre_release_number # 4 + +[bool] version.snapshot # true +[uint64] version.snapshot_sn # 1234567 +[string] version.snapshot_id # deadbeef +[string] version.snapshot_string # 1234567.deadbeef + +[uint64] version.revision # 3 +\ + +If the version is the latest snapshot (that is, it's in the \c{.z} form), then +the \c{version} module extracts the snapshot information from the version +control system used by the project. Currently only \c{git} is supported with +the following semantics. + +If the project's source directory (\c{src_root}) is clean (that is, it does +not have any changed or untracked files), then the \c{HEAD} commit date and +id are used as the snapshot sn and id, respectively. Otherwise, the snapshot +is left in the \c{.z} form (which signals the latest/uncommitted +snapshot). While we can work with such a \c{.z} snapshot locally, preparing a +distribution of such an uncommitted snapshot is an error. + +When we prepare a distribution of a snapshot, the \c{version} module +automatically adjusts the package name to include the snapshot information as +well as patches the manifest file in the distribution with the snapshot sn and +id (that is, replacing \c{.z} in the version value with the actual snapshot +information). The result is a package that is specific to this commit. + " |