From 593fd960891027b97567b2622ed4b6c16070ab36 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 28 Apr 2017 08:33:42 +0200 Subject: Implement support for pre-processing version headers (or other files) Also implement the build system version check. --- build2/version/rule.cxx | 474 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 445 insertions(+), 29 deletions(-) (limited to 'build2/version/rule.cxx') diff --git a/build2/version/rule.cxx b/build2/version/rule.cxx index b6f09de..6909b92 100644 --- a/build2/version/rule.cxx +++ b/build2/version/rule.cxx @@ -4,9 +4,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -20,10 +22,35 @@ namespace build2 { namespace version { + // Return true if this prerequisite looks like a project's manifest file. + // To be sure we would need to search it into target but that we can't + // do in match(). + // + static inline bool + manifest_prerequisite (const scope& rs, const prerequisite_member& p) + { + if (!p.is_a () || p.name () != "manifest") + return false; + + const scope& s (p.scope ()); + + if (s.root_scope () == nullptr) // Out of project prerequisite. + return false; + + dir_path d (p.dir ()); + if (d.relative ()) + d = s.src_path () / d; + d.normalize (); + + return d == rs.src_path (); + } + + // version_doc + // match_result version_doc:: match (action a, target& xt, const string&) const { - tracer trace ("version::version_file::match"); + tracer trace ("version::version_doc::match"); doc& t (static_cast (xt)); @@ -41,20 +68,8 @@ namespace build2 for (prerequisite_member p: group_prerequisite_members (a, t)) { - if (!p.is_a ()) - continue; - - const target& pt (p.search (t)); - - 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; + if (manifest_prerequisite (rs, p)) + return true; } l4 ([&]{trace << "no manifest prerequisite for target " << t;}); @@ -66,7 +81,7 @@ namespace build2 { doc& t (static_cast (xt)); - // Derive file names for the members. + // Derive the file name. // t.derive_path (); @@ -90,7 +105,7 @@ namespace build2 perform_update (action a, const target& xt) { const doc& t (xt.as ()); - const path& f (t.path ()); + const path& tp (t.path ()); const scope& rs (t.root_scope ()); const module& m (*rs.modules.lookup (module::name)); @@ -102,46 +117,447 @@ namespace build2 // 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. + // We also have to compare the contents (essentially using the file as + // its own depdb) in case of a snapshot since we can go back and forth + // between committed and uncommitted state that doesn't depend on any of + // our prerequisites. // { - auto p (execute_prerequisites (a, t, t.load_mtime ())); + optional ts ( + execute_prerequisites (a, t, t.load_mtime ())); - if (!p.first) + if (ts) { - if (!m.version_patched || !exists (f)) - return p.second; + if (!m.version.snapshot ()) // Everything came from the manifest. + return *ts; try { - ifdstream ifs (f, fdopen_mode::in, ifdstream::badbit); + ifdstream ifs (tp, fdopen_mode::in, ifdstream::badbit); string s; getline (ifs, s); if (s == m.version.string_project ()) - return p.second; + return *ts; } catch (const io_error& e) { - fail << "unable to read " << f << ": " << e; + fail << "unable to read " << tp << ": " << e; } } } if (verb >= 2) - text << "cat >" << f; + text << "cat >" << tp; try { - ofdstream ofs (f); + ofdstream ofs (tp); + auto_rmfile arm (tp); ofs << m.version.string_project () << endl; ofs.close (); + arm.cancel (); + } + catch (const io_error& e) + { + fail << "unable to write " << tp << ": " << e; + } + + t.mtime (system_clock::now ()); + return target_state::changed; + } + + // version_in + // + match_result version_in:: + match (action a, target& xt, const string&) const + { + tracer trace ("version::version_in::match"); + + file& t (static_cast (xt)); + const scope& rs (t.root_scope ()); + + bool fm (false); // Found manifest. + bool fi (false); // Found in. + for (prerequisite_member p: group_prerequisite_members (a, t)) + { + fm = fm || manifest_prerequisite (rs, p); + fi = fi || p.is_a (); + } + + if (!fm) + l4 ([&]{trace << "no manifest prerequisite for target " << t;}); + + if (!fi) + l4 ([&]{trace << "no in file prerequisite for target " << t;}); + + return fm && fi; + } + + recipe version_in:: + apply (action a, target& xt) const + { + file& t (static_cast (xt)); + + // Derive the file name. + // + 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_depdb; // Standard clean. + default: return noop_recipe; // Configure update. + } + } + + target_state version_in:: + perform_update (action a, const target& xt) + { + tracer trace ("version::version_in::perform_update"); + + const file& t (xt.as ()); + const path& tp (t.path ()); + + const scope& rs (t.root_scope ()); + const module& m (*rs.modules.lookup (module::name)); + + // Determine if anything needs to be updated. + // + timestamp mt (t.load_mtime ()); + auto pr (execute_prerequisites (a, t, mt)); + + bool update (!pr.first); + target_state ts (update ? target_state::changed : *pr.first); + + const in& i (pr.second); + const path& ip (i.path ()); + + // We use depdb to track both the .in file and the potentially patched + // snapshot. + // + { + depdb dd (tp + ".d"); + + // First should come the rule name/version. + // + if (dd.expect ("version.in 1") != nullptr) + l4 ([&]{trace << "rule mismatch forcing update of " << t;}); + + // Then the .in file. + // + if (dd.expect (i.path ()) != nullptr) + l4 ([&]{trace << "in file mismatch forcing update of " << t;}); + + // Finally the snapshot info. + // + if (dd.expect (m.version.string_snapshot ()) != nullptr) + l4 ([&]{trace << "snapshot mismatch forcing update of " << t;}); + + // Update if depdb mismatch. + // + if (dd.writing () || dd.mtime () > mt) + update = true; + + dd.close (); + } + + // If nothing changed, then we are done. + // + if (!update) + return ts; + + const string& proj (cast (rs.vars[var_project])); + + // Perform substitutions for the project itself (normally the version.* + // variables but we allow anything set on the root scope). + // + auto subst_self = [&rs, &m] (const location& l, const string& s) + { + if (lookup x = rs.vars[s]) + { + // Call the string() function to convert the value. + // + value v (*x); + + return convert ( + functions.call ( + "string", vector_view (&v, 1), l)); + } + else + fail (l) << "undefined project variable '" << s << "'" << endf; + }; + + // Perform substitutions for a dependency. Here we recognize the + // following substitutions: + // + // $libfoo.version$ - textual version constraint. + // $libfoo.condition(VER[,SNAP])$ - numeric satisfaction condition. + // $libfoo.check(VER[,SNAP])$ - numeric satisfaction check (#if ...). + // + // Where VER is the version number macro and SNAP is the optional + // snapshot number macro (only needed if you plan to include snapshot + // informaton in your constraints). + // + auto subst_dep = [&m] (const location& l, + const string& n, + const string& s) + { + // For now we re-parse the constraint every time. Firstly because + // not all of them are necessarily in the standard form and secondly + // because of the MT-safety. + // + standard_version_constraint c; + + try + { + auto i (m.dependencies.find (n)); + + if (i == m.dependencies.end ()) + fail (l) << "unknown dependency '" << n << "'"; + + if (i->second.empty ()) + fail (l) << "no version constraint for dependency " << n; + + c = standard_version_constraint (i->second); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid version constraint for dependency " << n + << ": " << e; + } + + // Now substitute. + // + size_t i; + if (s == "version") + { + return c.string (); // Use normalized representation. + } + if (s.compare (0, (i = 6), "check(") == 0 || + s.compare (0, (i = 10), "condition(") == 0) + { + size_t j (s.find_first_of (",)", i)); + + if (j == string::npos || (s[j] == ',' && s.back () != ')')) + fail (l) << "missing closing ')'"; + + string vm (s, i, j - i); // VER macro. + string sm (s[j] == ',' // SNAP macro. + ? string (s, j + 1, s.size () - j - 2) + : string ()); + + trim (vm); + trim (sm); + + auto cond = [&l, &c, &vm, &sm] () -> string + { + auto& miv (c.min_version); + auto& mav (c.max_version); + + bool mio (c.min_open); + bool mao (c.max_open); + + if (sm.empty () && + ((miv && miv->snapshot ()) || + (mav && mav->snapshot ()))) + fail (l) << "snapshot macro required for " << c.string (); + + auto cmp = [] (const string& m, const char* o, uint64_t v) + { + return m + o + to_string (v) + "ULL"; + }; + + // Note that version orders everything among pre-releases (that + // E being 0/1). So the snapshot comparison is only necessary + // "inside" the same pre-release. + // + auto max_cmp = [&vm, &sm, mao, &mav, &cmp] (bool p = false) + { + string r; + + if (mav->snapshot ()) + { + r += (p ? "(" : ""); + + r += cmp (vm, " < ", mav->version) + " || ("; + r += cmp (vm, " == ", mav->version) + " && "; + r += cmp (sm, (mao ? " < " : " <= "), mav->snapshot_sn) + ")"; + + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mao ? " < " : " <= "), mav->version); + + return r; + }; + + auto min_cmp = [&vm, &sm, mio, &miv, &cmp] (bool p = false) + { + string r; + + if (miv->snapshot ()) + { + r += (p ? "(" : ""); + + r += cmp (vm, " > ", miv->version) + " || ("; + r += cmp (vm, " == ", miv->version) + " && "; + r += cmp (sm, (mio ? " > " : " >= "), miv->snapshot_sn) + ")"; + + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mio ? " > " : " >= "), miv->version); + + return r; + }; + + // < / <= + // + if (!miv) + return max_cmp (); + + // > / >= + // + if (!mav) + return min_cmp (); + + // == + // + if (*miv == *mav) + { + string r (cmp (vm, " == ", miv->version)); + + if (miv->snapshot ()) + r += " && " + cmp (sm, " == ", miv->snapshot_sn); + + return r; + } + + // range + // + return min_cmp (true) + " && " + max_cmp (true); + }; + + if (s[1] == 'o') // condition + return cond (); + + string r; + + r += "#if !(" + cond () + ")\n"; + r += "# error incompatible " + n + " version, "; + r += n + ' ' + c.string () + " is required\n"; + r += "#endif"; + + return r; + } + else + fail (l) << "unknown dependency substitution '" << s << "'" << endf; + }; + + if (verb >= 2) + text << "ver -o " << tp << ' ' << ip; + else if (verb) + text << "ver " << tp; + + // Read and process the file, one line at a time. + // + const char* what; + const path* whom; + try + { + what = "open"; whom = &ip; + ifdstream ifs (ip, fdopen_mode::in, ifdstream::badbit); + + what = "open"; whom = &tp; + ofdstream ofs (tp); + auto_rmfile arm (tp); + + string s; // Reuse the buffer. + for (size_t ln (1);; ++ln) + { + what = "read"; whom = &ip; + if (!getline (ifs, s)) + break; // Could not read anything, not even newline. + + const location l (&ip, ln); // Not tracking column for now. + + // Scan the line looking for substiutions in the $.$ + // form. Treat $$ as an escape sequence. + // + for (size_t b (0), n, d; b != (n = s.size ()); b += d) + { + d = 1; + + if (s[b] != '$') + continue; + + // Find the other end. + // + size_t e (b + 1); + for (; e != (n = s.size ()); ++e) + { + if (s[e] == '$') + { + if (e + 1 != n && s[e + 1] == '$') // Escape. + s.erase (e, 1); // Keep one, erase the other. + else + break; + } + } + + if (e == n) + fail (l) << "unterminated '$'"; + + if (e - b == 1) // Escape. + { + s.erase (b, 1); // Keep one, erase the other. + continue; + } + + // We have a substition with b pointing to the opening $ and e -- + // to the closing. Split it into the package name and the trailer. + // + size_t p (s.find ('.', b + 1)); + + if (p == string::npos || p > e) + fail (l) << "invalid substitution: missing package name"; + + string sn (s, b + 1, p - b - 1); + string st (s, p + 1, e - p - 1); + string sr (sn == proj + ? subst_self (l, st) + : subst_dep (l, sn, st)); + + // Patch the result in and adjust the delta. + // + s.replace (b, e - b + 1, sr); + d = sr.size (); + } + + what = "write"; whom = &tp; + ofs << s << endl; + } + + what = "close"; whom = &tp; + ofs.close (); + arm.cancel (); + + what = "close"; whom = &ip; + ifs.close (); } catch (const io_error& e) { - fail << "unable to write " << f << ": " << e; + fail << "unable to " << what << ' ' << whom << ": " << e; } t.mtime (system_clock::now ()); -- cgit v1.1