#!/usr/bin/env/python3

# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# See BUILD.gn in this directory for an explanation of what this script is for.

import argparse
import os
import stat
import sys
import shutil
import subprocess
import re

REMOVE_METADATA_SUFFIX_RE = re.compile(b"-[0-9a-f]*", re.I)


def expand_name(concise_name):
  return "lib%s.rlib" % concise_name


def main():
  parser = argparse.ArgumentParser("find_std_rlibs.py")
  parser.add_argument("--rust-bin-dir",
                      help="Path to Rust binaries",
                      required=True),
  parser.add_argument("--target", help="Rust target triple", required=False),
  parser.add_argument("--output",
                      help="Path to rlibs without suffixes",
                      required=True)
  parser.add_argument("--depfile", help="Path to write depfile", required=True)
  parser.add_argument("--depfile-target",
                      help="Target to key depfile around",
                      required=True)
  parser.add_argument("--stdlibs",
                      help="Expected list of standard library libraries")
  parser.add_argument("--skip-stdlibs",
                      help="Standard library files to skip",
                      default="")
  parser.add_argument("--expected-rustc-version",
                      help="The string we expect to be reported by 'rustc -V'")
  args = parser.parse_args()
  # Ensure we handle each rlib in the expected list exactly once.
  if args.stdlibs:
    rlibs_expected = [expand_name(x) for x in args.stdlibs.split(',')]
  else:
    rlibs_expected = None
  rlibs_to_skip = [expand_name(x) for x in args.skip_stdlibs.split(',')]
  # First, ask rustc to confirm it's the version expected.
  rustc = os.path.join(args.rust_bin_dir, "rustc")
  if args.expected_rustc_version:
    proc = subprocess.run([rustc, "-V"], capture_output=True, text=True)
    proc.check_returncode()
    rustc_version = proc.stdout.rstrip()
    if rustc_version != args.expected_rustc_version:
      raise Exception("gn arguments state that the rustc_version is %s "
                      "but it was actually %s. Please adjust your "
                      "gn arguments to match." %
                      (args.expected_rustc_version, rustc_version))
  # Ask rustc where to find the stdlib for this target.
  rustc_args = [rustc, "--print", "target-libdir"]
  if args.target:
    rustc_args.extend(["--target", args.target])
  rustlib_dir = subprocess.check_output(rustc_args).rstrip()
  # Copy the rlibs to a predictable location. Whilst we're doing so,
  # also write a .d file so that ninja knows it doesn't need to do this
  # again unless the source rlibs change.
  # Format:
  # <output path to>/lib<lib name.rlib>: <path to each Rust stlib rlib>
  with open(args.depfile, 'w') as depfile:
    # Ninja isn't versatile at understanding depfiles. We have to say that a
    # single output depends on all the inputs. We choose any one of the
    # output rlibs for that purpose. If any of the input rlibs change, ninja
    # will run this script again and we'll copy them all afresh.
    depfile.write("%s:" %
                  (os.path.join(args.output, expand_name(args.depfile_target))))
    for f in os.listdir(rustlib_dir):
      if f.endswith(b'.rlib'):
        # As standard Rust includes a hash on the end of each filename
        # representing certain metadata, to ensure that clients will link
        # against the correct version. As gn will be manually passing
        # the correct file path to our linker invocations, we don't need
        # that, and it would prevent us having the predictable filenames
        # which we need for statically computable gn dependency rules.
        (concise_name, count) = REMOVE_METADATA_SUFFIX_RE.subn(b"", f)
        if count == 0:
          raise Exception("Unable to remove suffix from %s" % f)
        if concise_name.decode() in rlibs_to_skip:
          continue
        if rlibs_expected is not None:
          if concise_name.decode() not in rlibs_expected:
            raise Exception("Found stdlib rlib that wasn't expected: %s" %
                            concise_name)
          rlibs_expected.remove(concise_name.decode())
        infile = os.path.join(rustlib_dir, f)
        outfile = os.path.join(str.encode(args.output), concise_name)
        depfile.write(" %s" % (infile.decode()))
        if (not os.path.exists(outfile)
            or os.stat(infile).st_mtime != os.stat(outfile).st_mtime):
          if os.path.exists(outfile):
            st = os.stat(outfile)
            os.chmod(outfile, st.st_mode | stat.S_IWUSR)
          shutil.copy(infile, outfile)
    depfile.write("\n")
    if rlibs_expected:
      raise Exception("We failed to find all expected stdlib rlibs: %s" %
                      ','.join(rlibs_expected))


if __name__ == '__main__':
  sys.exit(main())
