diff options
Diffstat (limited to 'libbuild2/cc/windows-rpath.cxx')
-rw-r--r-- | libbuild2/cc/windows-rpath.cxx | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/libbuild2/cc/windows-rpath.cxx b/libbuild2/cc/windows-rpath.cxx new file mode 100644 index 0000000..5583315 --- /dev/null +++ b/libbuild2/cc/windows-rpath.cxx @@ -0,0 +1,400 @@ +// file : libbuild2/cc/windows-rpath.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <errno.h> // E* + +#include <libbuild2/scope.hxx> +#include <libbuild2/context.hxx> +#include <libbuild2/variable.hxx> +#include <libbuild2/algorithm.hxx> +#include <libbuild2/filesystem.hxx> +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/bin/target.hxx> + +#include <libbuild2/cc/link-rule.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace cc + { + // Provide limited emulation of the rpath functionality on Windows using a + // side-by-side assembly. In a nutshell, the idea is to create an assembly + // with links to all the prerequisite DLLs. + // + // 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 (e.g., in Wine). + // + // 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. + // + using namespace bin; + + // 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. + // + timestamp link_rule:: + windows_rpath_timestamp (const file& t, + const scope& bs, + action a, + linfo li) const + { + timestamp r (timestamp_nonexistent); + + // We need to collect all the DLLs, so go into implementation of both + // shared and static (in case they depend on shared). + // + auto imp = [] (const file&, bool) {return true;}; + + auto lib = [&r] (const file* const* lc, + const string& f, + lflags, + bool sys) + { + const file* l (lc != nullptr ? *lc : nullptr); + + // We don't rpath system libraries. + // + if (sys) + return; + + // Skip static libraries. + // + if (l != nullptr) + { + // This can be an "undiscovered" DLL (see search_library()). + // + if (!l->is_a<libs> () || l->path ().empty ()) // Also covers binless. + return; + } + else + { + // This is an absolute path and we need to decide whether it is + // a shared or static library. + // + // @@ This is so broken: we don't link to DLLs, we link to .lib or + // .dll.a! Should we even bother? Maybe only for "our" DLLs + // (.dll.lib/.dll.a)? But the DLL can also be in a different + // directory (lib/../bin). + // + // Though this can happen on MinGW with direct DLL link... + // + size_t p (path::traits_type::find_extension (f)); + + if (p == string::npos || casecmp (f.c_str () + p + 1, "dll") != 0) + return; + } + + // Ok, this is a DLL. + // + timestamp t (l != nullptr + ? l->load_mtime () + : mtime (f.c_str ())); + + if (t > r) + r = t; + }; + + for (const prerequisite_target& pt: t.prerequisite_targets[a]) + { + if (pt == nullptr || pt.adhoc) + continue; + + bool la; + const file* f; + + if ((la = (f = pt->is_a<liba> ())) || + (la = (f = pt->is_a<libux> ())) || // See through. + ( f = pt->is_a<libs> ())) + process_libraries (a, bs, li, sys_lib_dirs, + *f, la, pt.data, + imp, lib, nullptr, true); + } + + return r; + } + + // Like *_timestamp() but actually collect the DLLs (and weed out the + // duplicates). + // + auto link_rule:: + windows_rpath_dlls (const file& t, + const scope& bs, + action a, + linfo li) const -> windows_dlls + { + windows_dlls r; + + auto imp = [] (const file&, bool) {return true;}; + + auto lib = [&r, &bs] (const file* const* lc, + const string& f, + lflags, + bool sys) + { + const file* l (lc != nullptr ? *lc : nullptr); + + if (sys) + return; + + if (l != nullptr) + { + if (l->is_a<libs> () && !l->path ().empty ()) // Also covers binless. + { + // Get .pdb if there is one. + // + const target_type* tt (bs.find_target_type ("pdb")); + const target* pdb (tt != nullptr + ? find_adhoc_member (*l, *tt) + : nullptr); + r.insert ( + windows_dll { + f, + pdb != nullptr ? &pdb->as<file> ().path ().string () : nullptr, + string () + }); + } + } + else + { + size_t p (path::traits_type::find_extension (f)); + + if (p != string::npos && casecmp (f.c_str () + p + 1, "dll") == 0) + { + // See if we can find a corresponding .pdb. + // + windows_dll wd {f, nullptr, string ()}; + string& pdb (wd.pdb_storage); + + // First try "our" naming: foo.dll.pdb. + // + pdb = f; + pdb += ".pdb"; + + if (!exists (path (pdb))) + { + // Then try the usual naming: foo.pdb. + // + pdb.assign (f, 0, p); + pdb += ".pdb"; + + if (!exists (path (pdb))) + pdb.clear (); + } + + if (!pdb.empty ()) + wd.pdb = &pdb; + + r.insert (move (wd)); + } + } + }; + + for (const prerequisite_target& pt: t.prerequisite_targets[a]) + { + if (pt == nullptr || pt.adhoc) + continue; + + bool la; + const file* f; + + if ((la = (f = pt->is_a<liba> ())) || + (la = (f = pt->is_a<libux> ())) || // See through. + ( f = pt->is_a<libs> ())) + process_libraries (a, bs, li, sys_lib_dirs, + *f, la, pt.data, + imp, lib, nullptr, true); + } + + return r; + } + + const char* + windows_manifest_arch (const string& tcpu); // windows-manifest.cxx + + // The ts argument should be the DLLs timestamp returned by *_timestamp(). + // + // 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 DLLs timestamp against the assembly + // manifest file. + // + void link_rule:: + windows_rpath_assembly (const file& t, + const scope& bs, + action a, + linfo li, + const string& tcpu, + timestamp ts, + bool scratch) const + { + // Assembly paths and name. + // + dir_path ad (path_cast<dir_path> (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 *_dlls() call + // below which allocates memory, etc. + // + if (!scratch) + { + // The corner case here is when _timestamp() 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. Actually this can happen when + // switching to update-for-install. + // + if (ts != timestamp_nonexistent && ts <= mtime (am)) + return; + } + + // 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 aren't/no longer are any DLLs which means + // we just need to clean things up. + // + bool empty (ts == timestamp_nonexistent); + + windows_dlls dlls; + if (!empty) + dlls = windows_rpath_dlls (t, bs, a, li); + + // 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 (rmdir_r (t.ctx, ad, empty, 3)); + + if (empty) + return; + + if (s == rmdir_status::not_exist) + mkdir (ad, 3); + } + + // Symlink or copy the DLLs. + // + { + const scope& as (t.weak_scope ()); // Amalgamation. + + auto link = [&as] (const path& f, const path& l) + { + auto print = [&f, &l] (const char* cmd) + { + if (verb >= 3) + text << cmd << ' ' << f << ' ' << l; + }; + + // 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. + // + // 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. + // + try + { + switch (mkanylink (f, l, + true /* copy */, + f.sub (as.out_path ()) /* relative */)) + { + case entry_type::regular: print ("cp"); break; + case entry_type::symlink: print ("ln -s"); break; + case entry_type::other: print ("ln"); break; + default: assert (false); + } + } + catch (const pair<entry_type, system_error>& e) + { + const char* w (nullptr); + switch (e.first) + { + case entry_type::regular: print ("cp"); w = "copy"; break; + case entry_type::symlink: print ("ln -s"); w = "symlink"; break; + case entry_type::other: print ("ln"); w = "hardlink"; break; + default: assert (false); + } + + fail << "unable to make " << w << ' ' << l << ": " << e.second; + } + }; + + for (const windows_dll& wd: dlls) + { + //@@ Would be nice to avoid copying. Perhaps reuse buffers + // by adding path::assign() and traits::leaf(). + // + path dp (wd.dll); // DLL path. + path dn (dp.leaf ()); // DLL name. + + link (dp, ad / dn); + + // Link .pdb if there is one. + // + if (wd.pdb != nullptr) + { + path pp (*wd.pdb); + link (pp, ad / pp.leaf ()); + } + } + } + + if (verb >= 3) + text << "cat >" << am; + + if (t.ctx.dry_run) + return; + + auto_rmfile rm (am); + + try + { + ofdstream os (am); + + const char* pa (windows_manifest_arch (tcpu)); + + os << "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n" + << "<assembly xmlns='urn:schemas-microsoft-com:asm.v1'\n" + << " manifestVersion='1.0'>\n" + << " <assemblyIdentity name='" << an << "'\n" + << " type='win32'\n" + << " processorArchitecture='" << pa << "'\n" + << " version='0.0.0.0'/>\n"; + + + + for (const windows_dll& wd: dlls) + os << " <file name='" << path (wd.dll).leaf () << "'/>\n"; + + os << "</assembly>\n"; + + os.close (); + rm.cancel (); + } + catch (const io_error& e) + { + fail << "unable to write to " << am << ": " << e; + } + } + } +} |