# Copyright 2018 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.


from . import util

def compile_swiftshader(api, extra_tokens, swiftshader_root, ninja_root, cc, cxx, out):
  """Build SwiftShader with CMake.

  Building SwiftShader works differently from any other Skia third_party lib.
  See discussion in skbug.com/40034635 for more detail.

  Args:
    swiftshader_root: root of the SwiftShader checkout.
    ninja_root: A folder containing a ninja binary
    cc, cxx: compiler binaries to use
    out: target directory for libvk_swiftshader.so
  """
  swiftshader_opts = [
      '-DSWIFTSHADER_BUILD_TESTS=OFF',
      '-DSWIFTSHADER_WARNINGS_AS_ERRORS=OFF',
      '-DREACTOR_ENABLE_MEMORY_SANITIZER_INSTRUMENTATION=OFF',  # Way too slow.
  ]
  env = {
      'CC': cc,
      'CXX': cxx,
      'PATH': api.path.pathsep.join([str(ninja_root), "%(PATH)s"]),
      # We arrange our MSAN/TSAN prebuilts a little differently than
      # SwiftShader's CMakeLists.txt expects, so we'll just keep our custom
      # setup (everything mentioning libcxx below) and point SwiftShader's
      # CMakeLists.txt at a harmless non-existent path.
      'SWIFTSHADER_MSAN_INSTRUMENTED_LIBCXX_PATH': '/totally/phony/path',
  }

  # Extra flags for MSAN/TSAN, if necessary.
  san = None
  if 'MSAN' in extra_tokens:
    san = ('msan','memory')

  if san:
    short,full = san
    clang_linux = str(api.vars.workdir.joinpath('clang_linux'))
    libcxx = clang_linux + '/' + short
    cflags = ' '.join([
      '-fsanitize=' + full,
      '-stdlib=libc++',
      '-L%s/lib' % libcxx,
      '-lc++abi',
      '-I%s/include' % libcxx,
      '-I%s/include/c++/v1' % libcxx,
      '-Wno-unused-command-line-argument'  # Are -lc++abi and -Llibcxx/lib always unused?
    ])
    swiftshader_opts.extend([
      '-DSWIFTSHADER_{}=ON'.format(short.upper()),
      '-DCMAKE_C_FLAGS=%s' % cflags,
      '-DCMAKE_CXX_FLAGS=%s' % cflags,
    ])

  # Build SwiftShader.
  api.file.ensure_directory('makedirs swiftshader_out', out)
  with api.context(cwd=out, env=env):
    api.run(api.step, 'swiftshader cmake',
            cmd=['cmake'] + swiftshader_opts + [swiftshader_root, '-GNinja'])
    # See https://swiftshader-review.googlesource.com/c/SwiftShader/+/56452 for when the
    # deprecated targets were added. See skbug.com/40043473 for longer-term plans.
    api.run(api.step, 'swiftshader ninja', cmd=['ninja', '-C', out, 'vk_swiftshader'])


def get_compile_flags(api, checkout_root, out_dir, workdir):
  skia_dir      = checkout_root.joinpath('skia')
  compiler      = api.vars.builder_cfg.get('compiler',      '')
  configuration = api.vars.builder_cfg.get('configuration', '')
  extra_tokens  = api.vars.extra_tokens
  os            = api.vars.builder_cfg.get('os',            '')
  target_arch   = api.vars.builder_cfg.get('target_arch',   '')

  clang_linux      = str(workdir.joinpath('clang_linux'))
  if 'MSAN' in extra_tokens:
    clang_linux = str(workdir.joinpath('clang_ubuntu_noble'))
  win_toolchain    = str(workdir.joinpath('win_toolchain'))
  dwritecore       = str(workdir.joinpath('dwritecore'))

  cc, cxx, ccache = None, None, None
  extra_cflags = []
  extra_ldflags = []
  args = {
      'is_trivial_abi': 'true',
      'link_pool_depth': '2',
      'werror': 'true',
  }
  env = {}

  if os == 'Mac':
    extra_cflags.append(
        '-DREBUILD_IF_CHANGED_xcode_build_version=%s' % api.xcode.version)
    if 'iOS18' in extra_tokens:
      env['IPHONEOS_DEPLOYMENT_TARGET'] = '18.2'
      args['ios_min_target'] = '"18.0"'
    elif 'iOS' in extra_tokens:
      env['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
      args['ios_min_target'] = '"13.0"'
    else:
      # We have some machines on 11.
      env['MACOSX_DEPLOYMENT_TARGET'] = '11.0'

  # ccache + clang-tidy.sh chokes on the argument list.
  if (api.vars.is_linux or os == 'Mac') and 'Tidy' not in extra_tokens:
    if api.vars.is_linux:
      ccache = workdir.joinpath('ccache_linux', 'bin', 'ccache')
      # As of 2020-02-07, the sum of each Debian10-Clang-x86
      # non-flutter/android/chromebook build takes less than 75G cache space.
      env['CCACHE_MAXSIZE'] = '75G'
    else:
      ccache = workdir.joinpath('ccache_mac', 'bin', 'ccache')
      # As of 2020-02-10, the sum of each Build-Mac-Clang- non-android build
      # takes ~30G cache space.
      env['CCACHE_MAXSIZE'] = '50G'

    args['cc_wrapper'] = '"%s"' % ccache

    env['CCACHE_DIR'] = workdir.joinpath('cache', 'ccache')
    env['CCACHE_MAXFILES'] = '0'
    # Compilers are unpacked from cipd with bogus timestamps, only contribute
    # compiler content to hashes. If Ninja ever uses absolute paths to changing
    # directories we'll also need to set a CCACHE_BASEDIR.
    env['CCACHE_COMPILERCHECK'] = 'content'

  if compiler == 'Clang' and api.vars.is_linux:
    cc  = clang_linux + '/bin/clang'
    cxx = clang_linux + '/bin/clang++'
    extra_cflags .append('-B%s/bin' % clang_linux)
    extra_ldflags.append('-B%s/bin' % clang_linux)
    extra_ldflags.append('-fuse-ld=lld')
    extra_cflags.append('-DPLACEHOLDER_clang_linux_version=%s' %
                        api.run.asset_version('clang_linux', skia_dir))
    if 'Static' in extra_tokens:
      extra_ldflags.extend(['-static-libstdc++', '-static-libgcc'])

  elif compiler == 'Clang':
    cc, cxx = 'clang', 'clang++'
  elif compiler == 'GCC':
    cc, cxx = 'gcc', 'g++'
    # Newer GCC includes tons and tons of debugging symbols. This seems to
    # negatively affect our bots (potentially only in combination with other
    # bugs in Swarming or recipe code). Use g1 to reduce it a bit.
    extra_cflags.append('-g1')

  if 'Tidy' in extra_tokens:
    # Swap in clang-tidy.sh for clang++, but update PATH so it can find clang++.
    cxx = skia_dir.joinpath("tools/clang-tidy.sh")
    env['PATH'] = '%s:%%(PATH)s' % (clang_linux + '/bin')
    # Increase ClangTidy code coverage by enabling features.
    args.update({
      'skia_enable_fontmgr_empty':     'true',
      'skia_enable_graphite':          'true',
      'skia_enable_pdf':               'true',
      'skia_use_dawn':                 'true',
      'skia_use_expat':                'true',
      'skia_use_freetype':             'true',
      'skia_use_vulkan':               'true',
    })

  if 'Fuzz' in extra_tokens:
    cc, cxx = 'clang', 'clang++'
    args['skia_build_fuzzers'] = 'true'

  if 'Coverage' in extra_tokens:
    # See https://clang.llvm.org/docs/SourceBasedCodeCoverage.html for
    # more info on using llvm to gather coverage information.
    extra_cflags.append('-fprofile-instr-generate')
    extra_cflags.append('-fcoverage-mapping')
    extra_ldflags.append('-fprofile-instr-generate')
    extra_ldflags.append('-fcoverage-mapping')

  if compiler != 'MSVC' and configuration == 'Debug':
    extra_cflags.append('-O1')
  if compiler != 'MSVC' and configuration == 'OptimizeForSize':
    # build IDs are required for Bloaty if we want to use strip to ignore debug symbols.
    # https://github.com/google/bloaty/blob/master/doc/using.md#debugging-stripped-binaries
    extra_ldflags.append('-Wl,--build-id=sha1')
    args.update({
      'skia_use_runtime_icu': 'true',
      'skia_enable_optimize_size': 'true',
      'skia_use_jpeg_gainmaps': 'false',
    })

  if 'Exceptions' in extra_tokens:
    extra_cflags.append('/EHsc')
  if 'Fast' in extra_tokens:
    extra_cflags.extend(['-march=native', '-fomit-frame-pointer', '-O3',
                         '-ffp-contract=off'])

  if len(extra_tokens) == 1 and extra_tokens[0].startswith('SK'):
    extra_cflags.append('-D' + extra_tokens[0])
    # If we're limiting Skia at all, drop skcms to portable code.
    if 'SK_CPU_LIMIT' in extra_tokens[0]:
      extra_cflags.append('-DSKCMS_PORTABLE')

  if 'MSAN' in extra_tokens:
    extra_ldflags.append('-L' + clang_linux + '/msan')
  elif 'TSAN' in extra_tokens:
    extra_ldflags.append('-L' + clang_linux + '/tsan')
  elif api.vars.is_linux and compiler == 'Clang':
    extra_ldflags.append('-L' + clang_linux + '/lib')

  if configuration != 'Debug':
    args['is_debug'] = 'false'
  if 'Dawn' in extra_tokens:
    util.set_dawn_args_and_env(args, env, api, extra_tokens, skia_dir)
  if 'ANGLE' in extra_tokens:
    args['skia_use_angle'] = 'true'
  if 'SwiftShader' in extra_tokens:
    swiftshader_root = skia_dir.joinpath('third_party', 'externals', 'swiftshader')
    # Swiftshader will need to have ninja be on the path
    ninja_root = skia_dir.joinpath('third_party', 'ninja')
    swiftshader_out = out_dir.joinpath('swiftshader_out')
    compile_swiftshader(api, extra_tokens, swiftshader_root, ninja_root, cc, cxx, swiftshader_out)
    args['skia_use_vulkan'] = 'true'
    extra_cflags.extend(['-DSK_GPU_TOOLS_VK_LIBRARY_NAME=%s' %
        api.vars.swarming_out_dir.joinpath('swiftshader_out', 'libvk_swiftshader.so'),
    ])
  if 'MSAN' in extra_tokens:
    args['skia_use_fontconfig'] = 'false'
  if 'ASAN' in extra_tokens:
    args['skia_enable_spirv_validation'] = 'false'
  if 'NoPrecompile' in extra_tokens:
    args['skia_enable_precompile'] = 'false'
  if 'Graphite' in extra_tokens:
    args['skia_enable_graphite'] = 'true'
  if 'Vello' in extra_tokens:
    args['skia_enable_vello_shaders'] = 'true'
  if 'Fontations' in extra_tokens:
    args['skia_use_fontations'] = 'true'
    args['skia_use_freetype'] = 'true' # we compare with freetype in tests
    args['skia_use_system_freetype2'] = 'false'
  if 'RustPNG' in extra_tokens:
    args['skia_use_rust_png_decode'] = 'true'
    args['skia_use_rust_png_encode'] = 'true'
    args['skia_use_libpng_decode'] = 'false'
    # TODO(b/356875275) set skia_use_libpng_encode to false also
  if 'FreeType' in extra_tokens:
    args['skia_use_freetype'] = 'true'
    args['skia_use_system_freetype2'] = 'false'
    extra_cflags.extend(['-DSK_USE_FREETYPE_EMBOLDEN'])

  if 'NoGPU' in extra_tokens:
    args['skia_enable_ganesh'] = 'false'
  if 'NoDEPS' in extra_tokens:
    args.update({
      'is_official_build':             'true',
      'skia_enable_fontmgr_empty':     'true',
      'skia_enable_ganesh':            'true',

      'skia_enable_pdf':               'false',
      'skia_use_expat':                'false',
      'skia_use_freetype':             'false',
      'skia_use_harfbuzz':             'false',
      'skia_use_icu':                  'false',
      'skia_use_libjpeg_turbo_decode': 'false',
      'skia_use_libjpeg_turbo_encode': 'false',
      'skia_use_libpng_decode':        'false',
      'skia_use_libpng_encode':        'false',
      'skia_use_libwebp_decode':       'false',
      'skia_use_libwebp_encode':       'false',
      'skia_use_vulkan':               'false',
      'skia_use_wuffs':                'false',
      'skia_use_zlib':                 'false',
    })
  elif configuration != 'OptimizeForSize':
    args.update({
      'skia_use_client_icu': 'true',
      'skia_use_libgrapheme': 'true',
    })

  if 'Fontations' in extra_tokens:
    args['skia_use_icu4x'] = 'true'

  if 'Shared' in extra_tokens:
    args['is_component_build'] = 'true'
  if 'Vulkan' in extra_tokens and not 'Android' in extra_tokens and not 'Dawn' in extra_tokens:
    args['skia_use_vulkan'] = 'true'
    args['skia_enable_vulkan_debug_layers'] = 'true'
    # When running TSAN with Vulkan on NVidia, we experienced some timeouts. We found
    # a workaround (in GrContextFactory) that requires GL (in addition to Vulkan).
    if 'TSAN' in extra_tokens:
      args['skia_use_gl'] = 'true'
    else:
      args['skia_use_gl'] = 'false'
  if 'Direct3D' in extra_tokens and not 'Dawn' in extra_tokens:
    args['skia_use_direct3d'] = 'true'
    args['skia_use_gl'] = 'false'
  if 'Metal' in extra_tokens and not 'Dawn' in extra_tokens:
    args['skia_use_metal'] = 'true'
    args['skia_use_gl'] = 'false'
  if 'iOS' in extra_tokens or 'iOS18' in extra_tokens:
    # Bots use Chromium signing cert.
    args['skia_ios_identity'] = '".*83FNP.*"'
    # Get mobileprovision via the CIPD package.
    args['skia_ios_profile'] = '"%s"' % workdir.joinpath(
        'provisioning_profile_ios',
        'Upstream_Com_Testing_Provisioning_Profile.mobileprovision')
  if compiler == 'Clang' and 'Win' in os:
    args['clang_win'] = '"%s"' % workdir.joinpath('clang_win')
    extra_cflags.append('-DPLACEHOLDER_clang_win_version=%s' %
                        api.run.asset_version('clang_win', skia_dir))

  sanitize = ''
  for t in extra_tokens:
    if t.endswith('SAN'):
      sanitize = t
      if api.vars.is_linux and t == 'ASAN':
        # skbug.com/40040003 and skbug.com/40040004
        extra_cflags.append('-DSK_ENABLE_SCOPED_LSAN_SUPPRESSIONS')
  if 'SafeStack' in extra_tokens:
    assert sanitize == ''
    sanitize = 'safe-stack'

  if 'Wuffs' in extra_tokens:
    args['skia_use_wuffs'] = 'true'

  if 'AVIF' in extra_tokens:
    args['skia_use_libavif'] = 'true'

  for (k,v) in {
    'cc':  cc,
    'cxx': cxx,
    'sanitize': sanitize,
    'target_cpu': target_arch,
    'target_os': 'ios' if ('iOS' in extra_tokens or 'iOS18' in extra_tokens) else '',
    'win_sdk': win_toolchain + '/win_sdk' if 'Win' in os else '',
    'win_vc': win_toolchain + '/VC' if 'Win' in os else '',
    'skia_dwritecore_sdk': dwritecore if 'DWriteCore' in extra_tokens else '',
  }.items():
    if v:
      args[k] = '"%s"' % v
  if extra_cflags:
    args['extra_cflags'] = extra_cflags
  if extra_ldflags:
    args['extra_ldflags'] = extra_ldflags

  return args, env, ccache


def finalize_gn_flags(args):
  if args.get('extra_cflags'):
    args['extra_cflags'] = repr(args['extra_cflags']).replace("'", '"')
  if args.get('extra_ldflags'):
    args['extra_ldflags'] = repr(args['extra_ldflags']).replace("'", '"')
  return ' '.join('%s=%s' % (k,v) for (k,v) in sorted(args.items()))


def compile_fn(api, checkout_root, out_dir):
  skia_dir      = checkout_root.joinpath('skia')
  extra_tokens  = api.vars.extra_tokens

  with api.context(cwd=skia_dir):
    api.run(api.step, 'fetch-gn',
            cmd=['python3', skia_dir.joinpath('bin', 'fetch-gn')],
            infra_step=True)

    api.run(api.step, 'fetch-ninja',
            cmd=['python3', skia_dir.joinpath('bin', 'fetch-ninja')],
            infra_step=True)

  if api.vars.builder_cfg.get('os', '') in ('Mac'):
    api.xcode.install()

  workdir = api.path.start_dir
  args, env, ccache = get_compile_flags(api, checkout_root, out_dir, workdir)
  gn_args = finalize_gn_flags(args)
  gn = skia_dir.joinpath('bin', 'gn')
  ninja_root = skia_dir.joinpath('third_party', 'ninja')
  ninja = skia_dir.joinpath(ninja_root, 'ninja')

  # Putting ninja on the path makes it easier for subcommands to find it
  # (e.g. when building Dawn via CMake+ninja)
  # Importantly, this needs to go *after* depot_tools, so we append it
  existing_path = env.get('PATH', '%(PATH)s')
  env['PATH'] = api.path.pathsep.join([existing_path, str(ninja_root)])

  with api.context(cwd=skia_dir):
    with api.env(env):
      if ccache:
        api.run(api.step, 'ccache stats-start', cmd=[ccache, '-s'])
      api.run(api.step, 'gn gen',
              cmd=[gn, 'gen', out_dir, '--args=' + gn_args])
      if 'Fontations' in extra_tokens:
        api.run(api.step, 'gn clean',
              cmd=[gn, 'clean', out_dir])
      api.run(api.step, 'ninja', cmd=[ninja, '-C', out_dir])
      if ccache:
        api.run(api.step, 'ccache stats-end', cmd=[ccache, '-s'])


def copy_build_products(api, src, dst):
  util.copy_listed_files(api, src, dst, util.DEFAULT_BUILD_PRODUCTS)
  extra_tokens  = api.vars.extra_tokens
  os            = api.vars.builder_cfg.get('os', '')
  configuration = api.vars.builder_cfg.get('configuration', '')

  if 'SwiftShader' in extra_tokens:
    util.copy_listed_files(api,
        src.joinpath('swiftshader_out'),
        api.vars.swarming_out_dir.joinpath('swiftshader_out'),
        util.DEFAULT_BUILD_PRODUCTS)

  if configuration == 'OptimizeForSize':
    util.copy_listed_files(api, src, dst, ['skottie_tool_cpu', 'skottie_tool_gpu'])

  if os == 'Mac' and any('SAN' in t for t in extra_tokens):
    # The XSAN dylibs are in
    # Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib
    # /clang/11.0.0/lib/darwin, where 11.0.0 could change in future versions.
    xcode_clang_ver_dirs = api.file.listdir(
        'find XCode Clang version',
        api.vars.cache_dir.joinpath(
            'Xcode.app', 'Contents', 'Developer', 'Toolchains',
            'XcodeDefault.xctoolchain', 'usr', 'lib', 'clang'),
        test_data=['11.0.0'])
    # Allow both clang/16 and clang/16.0.0, so long as they are equivalent.
    assert len({api.path.realpath(d) for d in xcode_clang_ver_dirs}) == 1
    dylib_dir = xcode_clang_ver_dirs[0].joinpath('lib', 'darwin')
    dylibs = api.file.glob_paths('find xSAN dylibs', dylib_dir,
                                 'libclang_rt.*san_osx_dynamic.dylib',
                                 test_data=[
                                     'libclang_rt.asan_osx_dynamic.dylib',
                                     'libclang_rt.tsan_osx_dynamic.dylib',
                                     'libclang_rt.ubsan_osx_dynamic.dylib',
                                 ])
    for f in dylibs:
      api.file.copy('copy %s' % api.path.basename(f), f, dst)
