aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/cc/windows-rpath.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/cc/windows-rpath.cxx')
-rw-r--r--libbuild2/cc/windows-rpath.cxx400
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;
+ }
+ }
+ }
+}