text: // file      : libbuild2/cc/windows-rpath.cxx -*- C++ -*-
  // license   : MIT; see accompanying LICENSE file
  
  #include <cerrno> // 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);
  
        // Duplicate suppression similar to rpath_libraries().
        //
        rpathed_libraries ls;
  
        // 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 target&, bool) {return true;};
  
        auto lib = [&r, &ls] (
          const target* const* lc,
          const small_vector<reference_wrapper<const string>, 2>& ns,
          lflags,
          const string*,
          bool sys)
        {
          const file* l (lc != nullptr ? &(*lc)->as<file> () : nullptr);
  
          // We don't rpath system libraries.
          //
          if (sys)
            return false;
  
          if (l != nullptr)
          {
            // Suppress duplicates.
            //
            if (find (ls.begin (), ls.end (), l) != ls.end ())
              return false;
  
            // Ignore static libraries. Note that this can be an "undiscovered"
            // DLL (see search_library()).
            //
            if (l->is_a<libs> () && !l->path ().empty ()) // Also covers binless.
            {
              timestamp t (l->load_mtime ());
  
              if (t > r)
                r = t;
            }
  
            ls.push_back (l);
          }
          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...
            //
            for (const string& f: ns)
            {
              size_t p (path::traits_type::find_extension (f));
  
              if (p != string::npos && icasecmp (f.c_str () + p + 1, "dll") == 0)
              {
                timestamp t (mtime (f.c_str ()));
  
                if (t > r)
                  r = t;
              }
            }
          }
  
          return true;
        };
  
        library_cache lib_cache;
        for (const prerequisite_target& pt: t.prerequisite_targets[a])
        {
          if (pt.adhoc || pt == nullptr)
            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 /* self */,
                               &lib_cache);
        }
  
        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
      {
        // Note that we cannot reuse windows_dlls for duplicate suppression
        // since it would only keep track of shared libraries.
        //
        windows_dlls r;
        rpathed_libraries ls;
  
        struct
        {
          const scope&       bs;
          rpathed_libraries& ls;
        } d {bs, ls};
  
        auto imp = [] (const target&, bool) {return true;};
  
        auto lib = [&d, &r] (
          const target* const* lc,
          const small_vector<reference_wrapper<const string>, 2>& ns,
          lflags,
          const string*,
          bool sys)
        {
          const file* l (lc != nullptr ? &(*lc)->as<file> () : nullptr);
  
          if (sys)
            return false;
  
          if (l != nullptr)
          {
            // Suppress duplicates.
            //
            if (find (d.ls.begin (), d.ls.end (), l) != d.ls.end ())
              return false;
  
            if (l->is_a<libs> () && !l->path ().empty ()) // Also covers binless.
            {
              // Get .pdb if there is one.
              //
              const target_type* tt (d.bs.find_target_type ("pdb"));
              const target* pdb (tt != nullptr
                                 ? find_adhoc_member (*l, *tt)
                                 : nullptr);
  
              // Here we assume it's not a duplicate due to the check above.
              //
              r.push_back (
                windows_dll {
                  ns[0],
                  pdb != nullptr ? pdb->as<file> ().path ().string () : string (),
                });
            }
  
            d.ls.push_back (l);
          }
          else
          {
            string pdb;
            for (const string& f: ns)
            {
              size_t p (path::traits_type::find_extension (f));
  
              if (p != string::npos && icasecmp (f.c_str () + p + 1, "dll") == 0)
              {
                if (find_if (r.begin (), r.end (),
                             [&f] (const windows_dll& e)
                             {
                               return e.dll.get () == f;
                             }) == r.end ())
                {
                  // See if we can find a corresponding .pdb. 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 ();
                  }
  
                  r.push_back (
                    windows_dll {f, pdb.empty () ? string () : move (pdb)});
                }
              }
            }
          }
  
          return true;
        };
  
        library_cache lib_cache;
        for (const prerequisite_target& pt: t.prerequisite_targets[a])
        {
          if (pt.adhoc || pt == nullptr)
            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 /* self */,
                               &lib_cache);
        }
  
        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.get ()); // DLL path.
            path dn (dp.leaf ());    // DLL name.
  
            link (dp, ad / dn);
  
            // Link .pdb if there is one.
            //
            if (!wd.pdb.empty ())
            {
              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;
        }
      }
    }
  }
  