/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <sys/stat.h>
#include <sysexits.h>

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

#include "android-base/parsebool.h"
#include "android-base/stringprintf.h"
#include "android-base/strings.h"
#include "arch/instruction_set.h"
#include "base/file_utils.h"
#include "base/globals.h"
#include "base/macros.h"
#include "base/os.h"
#include "base/testing.h"

namespace art {
namespace gbi {

namespace {

using ::android::base::ConsumePrefix;
using ::android::base::ConsumeSuffix;
using ::android::base::Join;
using ::android::base::ParseBool;
using ::android::base::ParseBoolResult;
using ::android::base::StringPrintf;
using ::art::testing::GetLibCoreDexFileNames;
using ::art::testing::GetLibCoreDexLocations;

constexpr const char* kUsage = R"(
A commandline tool to generate a primary boot image for testing.

Usage: generate-boot-image --output-dir=OUTPUT_DIR [OPTIONS]... [-- [DEX2OAT_OPTIONS]...]

Supported options:
  --help: Print this text.
  --output-dir=OUTPUT_DIR: The directory to output the boot image. Required.
  --compiler-filter=COMPILER_FILTER: The compiler filter option to pass to dex2oat. Default: verify
  --use-profile=true|false: If true, use a profile. Default: true
  --dex2oat-bin=DEX2OAT_BIN: The path to the dex2oat binary. Required when running on host. Default
      on target: /apex/com.android.art/bin/dex2oat{32,64,32d,64d}
  --android-root=ANDROID_ROOT: The root directory to search for bootclasspath jars. The file
      structure under the root must be in the form of:
      /apex
        /com.android.art
          /javalib
            /core-oj.jar
            ...
        /com.android.i18n
          /javalib
            ...
        /com.android.conscrypt
          /javalib
            ...
      Required when running on host. Default on target: /
  --profile-file=PROFILE_FILE: The path to the profile file. Required when running on host and
      --use-profile is true. Default on target: /apex/com.android.art/etc/boot-image.prof
  --instruction-set=ISA: The instruction set option to pass to dex2oat. Required when running on
      host. The default on target is based on the ISA of this binary.
  --core-only=true|false: If true, only compile ART jars. Otherwise, also compile core-icu4j and
      conscrypt. Default: false
  --: Arguments following '--' are directly passed to dex2oat.
)";

struct Options {
  std::string output_dir = "";
  // Set the compiler filter to `verify` by default to make test preparation
  // faster.
  std::string compiler_filter = "verify";
  bool use_profile = true;
  std::string dex2oat_bin = "";
  std::string android_root = "";
  std::string profile_file = "";
  std::string instruction_set = "";
  bool core_only = false;
  std::vector<std::string> dex2oat_options;
};

[[noreturn]] void Usage(const std::string& message) {
  LOG(ERROR) << message << '\n';
  std::cerr << message << "\n" << kUsage << "\n";
  exit(EX_USAGE);
}

std::string GetCompilerExecutable() {
  std::string compiler_executable = GetArtBinDir() + "/dex2oat";
  if (kIsDebugBuild) {
    compiler_executable += 'd';
  }
  compiler_executable += Is64BitInstructionSet(kRuntimeISA) ? "64" : "32";
  return compiler_executable;
}

// Joins a list of commandline args into a single string, where each part is quoted with double
// quotes. Note that this is a naive implementation that does NOT escape existing double quotes,
// which is fine since we don't have existing double quotes in the args in this particular use case
// and this code is never used in production.
std::string BuildCommand(const std::vector<std::string>& args) {
  std::string command = "";
  for (const std::string& arg : args) {
    if (!command.empty()) {
      command += " ";
    }
    command += '"' + arg + '"';
  }
  return command;
}

int GenerateBootImage(const Options& options) {
  std::vector<std::string> args;
  args.push_back(options.dex2oat_bin);

  std::vector<std::string> dex_files =
      GetLibCoreDexFileNames(options.android_root, options.core_only);
  std::vector<std::string> dex_locations = GetLibCoreDexLocations(options.core_only);
  args.push_back("--runtime-arg");
  args.push_back("-Xbootclasspath:" + Join(dex_files, ":"));
  args.push_back("--runtime-arg");
  args.push_back("-Xbootclasspath-locations:" + Join(dex_locations, ":"));
  for (const std::string& file : dex_files) {
    args.push_back("--dex-file=" + file);
  }
  for (const std::string& location : dex_locations) {
    args.push_back("--dex-location=" + location);
  }

  args.push_back("--instruction-set=" + options.instruction_set);
  args.push_back(StringPrintf("--base=0x%08x", ART_BASE_ADDRESS));
  args.push_back("--compiler-filter=" + options.compiler_filter);
  if (options.use_profile) {
    args.push_back("--profile-file=" + options.profile_file);
  }
  args.push_back("--avoid-storing-invocation");
  args.push_back("--generate-debug-info");
  args.push_back("--generate-build-id");
  args.push_back("--image-format=lz4hc");
  args.push_back("--strip");
  args.push_back("--android-root=out/empty");

  std::string path = ART_FORMAT("{}/{}", options.output_dir, options.instruction_set);
  if (!OS::DirectoryExists(path.c_str())) {
    CHECK_EQ(mkdir(path.c_str(), S_IRWXU), 0);
  }
  args.push_back(StringPrintf("--image=%s/boot.art", path.c_str()));
  args.push_back(StringPrintf("--oat-file=%s/boot.oat", path.c_str()));

  std::move(
      options.dex2oat_options.begin(), options.dex2oat_options.end(), std::back_inserter(args));

  int exit_code = system(BuildCommand(args).c_str());
  if (exit_code != 0) {
    LOG(ERROR) << "dex2oat invocation failed. Exit code: " << exit_code;
  }
  return exit_code;
}

int Main(int argc, char** argv) {
  android::base::InitLogging(argv, android::base::LogdLogger(android::base::SYSTEM));

  Options options;
  for (int i = 1; i < argc; i++) {
    std::string_view arg{argv[i]};
    if (arg == "--help") {
      std::cerr << kUsage << "\n";
      exit(0);
    } else if (ConsumePrefix(&arg, "--output-dir=")) {
      options.output_dir = arg;
    } else if (ConsumePrefix(&arg, "--compiler-filter=")) {
      options.compiler_filter = arg;
    } else if (ConsumePrefix(&arg, "--use-profile=")) {
      ParseBoolResult result = ParseBool(arg);
      if (result == ParseBoolResult::kError) {
        Usage(ART_FORMAT("Unrecognized --use-profile value: '{}'", arg));
      }
      options.use_profile = result == ParseBoolResult::kTrue;
    } else if (ConsumePrefix(&arg, "--dex2oat-bin=")) {
      options.dex2oat_bin = arg;
    } else if (ConsumePrefix(&arg, "--android-root=")) {
      ConsumeSuffix(&arg, "/");
      options.android_root = arg;
    } else if (ConsumePrefix(&arg, "--profile-file=")) {
      options.profile_file = arg;
    } else if (ConsumePrefix(&arg, "--instruction-set=")) {
      options.instruction_set = arg;
    } else if (ConsumePrefix(&arg, "--core-only=")) {
      ParseBoolResult result = ParseBool(arg);
      if (result == ParseBoolResult::kError) {
        Usage(ART_FORMAT("Unrecognized --core-only value: '{}'", arg));
      }
      options.core_only = result == ParseBoolResult::kTrue;
    } else if (arg == "--") {
      for (i++; i < argc; i++) {
        options.dex2oat_options.push_back(argv[i]);
      }
    } else {
      Usage(ART_FORMAT("Unrecognized argument: '{}'", argv[i]));
    }
  }

  if (options.output_dir.empty()) {
    Usage("--output-dir must be specified");
  }

  if (options.dex2oat_bin.empty()) {
    if (kIsTargetBuild) {
      options.dex2oat_bin = GetCompilerExecutable();
    } else {
      Usage("--dex2oat-bin must be specified when running on host");
    }
  }

  if (options.android_root.empty()) {
    if (!kIsTargetBuild) {
      Usage("--android-root must be specified when running on host");
    }
  }

  if (options.use_profile && options.profile_file.empty()) {
    if (kIsTargetBuild) {
      options.profile_file = ART_FORMAT("{}/etc/boot-image.prof", GetArtRoot());
    } else {
      Usage("--profile-file must be specified when running on host and --use-profile is true");
    }
  }

  if (options.instruction_set.empty()) {
    if (kIsTargetBuild) {
      options.instruction_set = GetInstructionSetString(kRuntimeISA);
    } else {
      Usage("--instruction-set must be specified when running on host");
    }
  }

  return GenerateBootImage(options);
}

}  // namespace

}  // namespace gbi
}  // namespace art

int main(int argc, char** argv) { return art::gbi::Main(argc, argv); }
