From 6205a2d9eb7db0a25959ae34dc5406f228da92a5 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 8 Jul 2016 14:05:28 +0200 Subject: Implement limited rpath emulation for Windows --- build2/cxx/link.cxx | 470 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 425 insertions(+), 45 deletions(-) (limited to 'build2/cxx/link.cxx') diff --git a/build2/cxx/link.cxx b/build2/cxx/link.cxx index 0e4fd5a..4de3194 100644 --- a/build2/cxx/link.cxx +++ b/build2/cxx/link.cxx @@ -4,6 +4,9 @@ #include +#include // E* + +#include #include // exit() #include @@ -898,7 +901,7 @@ namespace build2 switch (a) { case perform_update_id: return &perform_update; - case perform_clean_id: return &perform_clean_depdb; + case perform_clean_id: return &perform_clean; default: return noop_recipe; // Configure update. } } @@ -949,19 +952,339 @@ namespace build2 } } + // Provide limited emulation of the rpath functionality on Windows using a + // manifest and a side-by-side assembly. In a nutshell, the idea is to + // create an assembly with links to all the prerequisite DLLs. + // + // The scratch argument should be true if the DLL set has changed and we + // need to regenerate everything from scratch. Otherwise, we try to avoid + // unnecessary work by comparing the DLL timestamps against the assembly + // manifest file. + // + // If the manifest argument is false, then don't generate the target + // manifest (i.e., it will be embedded). + // + // Note that currently our assemblies contain all the DLLs that the + // executable depends on, recursively. The alternative approach could be + // to also create assemblies for DLLs. This appears to be possible (but we + // will have to use the resource ID 2 for such a manifest). And it will + // probably be necessary for DLLs that are loaded dynamically with + // LoadLibrary(). The tricky part is how such nested assemblies will be + // found. Since we are effectively (from the loader's point of view) + // copying the DLLs, we will also have to copy their assemblies (because + // the loader looks for them in the same directory as the DLL). It's not + // clear how well such nested assemblies are supported. + // + static timestamp + timestamp_dlls (target&); + + static void + collect_dlls (set&, target&); + + static void + emulate_rpath_windows (file& t, bool scratch, bool manifest) + { + // Assembly paths and name. + // + dir_path ad (path_cast (t.path () + ".dlls")); + string an (ad.leaf ().string ()); + path am (ad / path (an + ".manifest")); + + // First check if we actually need to do anything. Since most of the + // time we won't, we don't want to combine it with the collect_dlls() + // call below which allocates memory, etc. + // + if (!scratch) + { + // The corner case here is when timestamp_dlls() returns nonexistent + // signalling that there aren't any DLLs but the assembly manifest + // file exists. This, however, can only happen if we somehow managed + // to transition from the "have DLLs" state to "no DLLs" without going + // through the from scratch update. And this shouldn't happen (famous + // last words before a core dump). + // + if (timestamp_dlls (t) <= file_mtime (am)) + return; + } + + scope& rs (t.root_scope ()); + + // Next collect the set of DLLs that will be in our assembly. We need to + // do this recursively which means we may end up with duplicates. Also, + // it is possible that there will (no longer) be any DLLs which means we + // just need to clean things up. + // + set dlls; + collect_dlls (dlls, t); + bool empty (dlls.empty ()); + + // Target manifest. + // + path tm; + if (manifest) + tm = t.path () + ".manifest"; + + // Clean the assembly directory and make sure it exists. Maybe it would + // have been faster to overwrite the existing manifest rather than + // removing the old one and creating a new one. But this is definitely + // simpler. + // + { + rmdir_status s (build2::rmdir_r (ad, empty, 3)); + + // What if there is a user-defined manifest in the src directory? We + // would just overwrite it if src == out. While we could add a comment + // with some signature that can be used to detect an auto-generated + // manifest, we can also use the presence of the assembly directory as + // such a marker. + // + // @@ And what can we do instead? One idea is for the user to call it + // something else and we merge the two. Perhaps the link rule could + // have support for manifests (i.e., manifest will be one of the + // prerequisites). A similar problem is with embedded vs standalone + // manifests (embedded preferred starting from Vista). I guess if we + // support embedding manifests, then we can also merge them. + // + if (manifest && + s == rmdir_status::not_exist && + rs.src_path () == rs.out_path () && + file_exists (tm)) + { + fail << tm << " looks like a custom manifest" << + info << "remove it manually if that's not the case"; + } + + if (empty) + { + if (manifest) + rmfile (tm, 3); + + return; + } + + if (s == rmdir_status::not_exist) + mkdir (ad, 3); + } + + // Translate the compiler target CPU value to the processorArchitecture + // attribute value. + // + const string& tcpu (cast (rs["cxx.target.cpu"])); + + const char* pa (tcpu == "i386" || tcpu == "i686" ? "x86" : + tcpu == "x86_64" ? "amd64" : + nullptr); + + if (pa == nullptr) + fail << "unable to translate CPU " << tcpu << " to manifest " + << "processor architecture"; + + if (verb >= 3) + text << "cat >" << am; + + try + { + ofstream ofs; + ofs.exceptions (ofstream::failbit | ofstream::badbit); + ofs.open (am.string ()); + + ofs << "\n" + << "\n" + << " \n"; + + scope& as (*rs.weak_scope ()); // Amalgamation scope. + + for (file* dt: dlls) + { + const path& dp (dt->path ()); // DLL path. + const path dn (dp.leaf ()); // DLL name. + const path lp (ad / dn); // Link path. + + auto print = [&dp, &lp] (const char* cmd) + { + if (verb >= 3) + text << cmd << ' ' << dp << ' ' << lp; + }; + + // First we try to create a symlink. If that fails (e.g., "Windows + // happens"), then we resort to hard links. If that doesn't work + // out either (e.g., not on the same filesystem), then we fall back + // to copies. So things are going to get a bit nested. + // + try + { + // For the symlink use a relative target path if both paths are + // part of the same amalgamation. This way if the amalgamation is + // moved as a whole, the links will remain valid. + // + if (dp.sub (as.out_path ())) + mksymlink (dp.relative (ad), lp); + else + mksymlink (dp, lp); + + print ("ln -s"); + } + catch (const system_error& e) + { + int c (e.code ().value ()); + + if (c != EPERM && c != ENOSYS) + { + print ("ln -s"); + fail << "unable to create symlink " << lp << ": " << e.what (); + } + + try + { + mkhardlink (dp, lp); + print ("ln"); + } + catch (const system_error& e) + { + int c (e.code ().value ()); + + if (c != EPERM && c != ENOSYS) + { + print ("ln"); + fail << "unable to create hard link " << lp << ": " + << e.what (); + } + + try + { + cpfile (dp, lp); + print ("cp"); + } + catch (const system_error& e) + { + print ("cp"); + fail << "unable to create copy " << lp << ": " << e.what (); + } + } + } + + ofs << " \n"; + } + + ofs << "\n"; + } + catch (const ofstream::failure&) + { + fail << "unable to write to " << am; + } + + // Create the manifest if requested. + // + if (!manifest) + return; + + if (verb >= 3) + text << "cat >" << tm; + + try + { + ofstream ofs; + ofs.exceptions (ofstream::failbit | ofstream::badbit); + ofs.open (tm.string (), ofstream::out | ofstream::trunc); + + ofs << "\n" + << "\n" + << "\n" + << " \n" + << " \n" + << " \n" + << " \n" + << " \n" + << " \n" + << "\n"; + } + catch (const ofstream::failure&) + { + fail << "unable to write to " << tm; + } + } + + // Return the greatest (newest) timestamp of all the DLLs that we will be + // adding to the assembly or timestamp_nonexistent if there aren't any. + // + static timestamp + timestamp_dlls (target& t) + { + timestamp r (timestamp_nonexistent); + + for (target* pt: t.prerequisite_targets) + { + if (libso* ls = pt->is_a ()) + { + // This can be an installed library in which case we will have just + // the import stub but may also have just the DLL. For now we don't + // bother with installed libraries. + // + if (ls->member == nullptr) + continue; + + file& dll (static_cast (*ls->member)); + + // What if the DLL is in the same directory as the executable, will + // it still be found even if there is an assembly? On the other + // hand, handling it as any other won't hurt us much. + // + timestamp t; + + if ((t = dll.mtime ()) > r) + r = t; + + if ((t = timestamp_dlls (*ls)) > r) + r = t; + } + } + + return r; + } + + static void + collect_dlls (set& s, target& t) + { + for (target* pt: t.prerequisite_targets) + { + if (libso* ls = pt->is_a ()) + { + if (ls->member == nullptr) + continue; + + file& dll (static_cast (*ls->member)); + + s.insert (&dll); + collect_dlls (s, *ls); + } + } + } + target_state link:: perform_update (action a, target& xt) { tracer trace ("cxx::link::perform_update"); - path_target& t (static_cast (xt)); + file& t (static_cast (xt)); type lt (link_type (t)); bool so (lt == type::so); // Update prerequisites. // - bool up (execute_prerequisites (a, t, t.mtime ())); + bool update (execute_prerequisites (a, t, t.mtime ())); scope& rs (t.root_scope ()); @@ -1059,51 +1382,57 @@ namespace build2 append_options (args, t, "cxx.coptions"); append_std (args, rs, cid, t, std); - // Set soname. + // Handle soname/rpath. Emulation for Windows is done after we have + // built the target. // - if (so && tclass != "windows") + if (tclass == "windows") { - const string& leaf (t.path ().leaf ().string ()); + auto l (t["bin.rpath"]); - if (tclass == "macosx") + if (l && !l->empty ()) + fail << cast (rs["cxx.target"]) << " does not have rpath"; + } + else + { + // Set soname. + // + if (so) { - // With Mac OS 10.5 (Leopard) Apple finally caved in and gave us - // a way to emulate vanilla -rpath. - // - // It may seem natural to do something different on update for - // install. However, if we don't make it @rpath, then the user - // won't be able to use config.bin.rpath for installed libraries. - // - soname1 = "-install_name"; - soname2 = "@rpath/" + leaf; - } - else - soname1 = "-Wl,-soname," + leaf; + const string& leaf (t.path ().leaf ().string ()); - if (!soname1.empty ()) - args.push_back (soname1.c_str ()); + if (tclass == "macosx") + { + // With Mac OS 10.5 (Leopard) Apple finally caved in and gave us + // a way to emulate vanilla -rpath. + // + // It may seem natural to do something different on update for + // install. However, if we don't make it @rpath, then the user + // won't be able to use config.bin.rpath for installed libraries. + // + soname1 = "-install_name"; + soname2 = "@rpath/" + leaf; + } + else + soname1 = "-Wl,-soname," + leaf; - if (!soname2.empty ()) - args.push_back (soname2.c_str ()); - } + if (!soname1.empty ()) + args.push_back (soname1.c_str ()); - // Add rpaths. We used to first add the ones specified by the user so - // that they take precedence. But that caused problems if we have old - // versions of the libraries sitting in the rpath location (e.g., - // installed libraries). And if you think about this, it's probably - // correct to prefer libraries that we explicitly imported to the - // ones found via rpath. - // - // Note also that if this is update for install, then we don't add - // rpath of the imported libraries (i.e., we assume the are also - // installed). - // - if (tclass == "windows") - { - // @@ VC TODO: emulate own rpath somehow and complain on user's. - } - else - { + if (!soname2.empty ()) + args.push_back (soname2.c_str ()); + } + + // Add rpaths. We used to first add the ones specified by the user + // so that they take precedence. But that caused problems if we have + // old versions of the libraries sitting in the rpath location + // (e.g., installed libraries). And if you think about this, it's + // probably correct to prefer libraries that we explicitly imported + // to the ones found via rpath. + // + // Note also that if this is update for install, then we don't add + // rpath of the imported libraries (i.e., we assume they are also + // installed). + // for (target* pt: t.prerequisite_targets) { if (libso* ls = pt->is_a ()) @@ -1188,17 +1517,19 @@ namespace build2 } // If any of the above checks resulted in a mismatch (different linker, - // options, or input file set), or if the database is newer than the - // target (interrupted update) then force the target update. + // options or input file set), or if the database is newer than the + // target (interrupted update) then force the target update. Also + // note this situation in the "from scratch" flag. // + bool scratch (false); if (dd.writing () || dd.mtime () > t.mtime ()) - up = true; + scratch = update = true; dd.close (); // If nothing changed, then we are done. // - if (!up) + if (!update) return target_state::unchanged; // Ok, so we are updating. Finish building the command line. @@ -1387,6 +1718,11 @@ namespace build2 throw failed (); } + // Remove the target file if any of the subsequent actions fail. If we + // don't do that, we will end up with a broken build that is up-to-date. + // + auto_rmfile rm (t.path ()); + if (ranlib) { const char* args[] = { @@ -1415,6 +1751,13 @@ namespace build2 } } + // Emulate rpath on Windows. + // + if (lt == type::e && tclass == "windows") + emulate_rpath_windows (t, scratch, cid != "msvc"); + + rm.cancel (); + // Should we go to the filesystem and get the new mtime? We know the // file has been modified, so instead just use the current clock time. // It has the advantage of having the subseconds precision. @@ -1423,6 +1766,43 @@ namespace build2 return target_state::changed; } + target_state link:: + perform_clean (action a, target& xt) + { + tracer trace ("cxx::link::perform_clean"); + + file& t (static_cast (xt)); + + type lt (link_type (t)); + + scope& rs (t.root_scope ()); + const string& cid (cast (rs["cxx.id"])); + const string& tclass (cast (rs["cxx.target.class"])); + + if (lt == type::e && tclass == "windows") + { + bool m (cid != "msvc"); + + // Check for custom manifest, just like in emulate_rpath_windows(). + // + if (m && + rs.src_path () == rs.out_path () && + file_exists (t.path () + ".manifest") && + !dir_exists (path_cast (t.path () + ".dlls"))) + { + fail << t.path () + ".manifest" << " looks like a custom manifest" << + info << "remove it manually if that's not the case"; + } + + return clean_extra ( + a, + t, + {"+.d", (m ? "+.manifest" : nullptr), "/+.dlls"}); + } + else + return clean_extra (a, t, {"+.d"}); + } + link link::instance; } } -- cgit v1.1