All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ants.vcpkg.sn-vcpkg_3.0.0.19.source-code.CommandLineApp.scala Maven / Gradle / Ivy

There is a newer version: 0.0.21
Show newest version
package com.indoorvivants.vcpkg
package cli

import cats.implicits.*
import com.indoorvivants.detective.Platform
import com.monovore.decline.*
import io.circe.Codec
import io.circe.CodecDerivation
import io.circe.Decoder
import java.io.File

enum Compiler:
  case Clang, ClangPP

enum Action:
  case Install(
      dependencies: VcpkgDependencies,
      out: OutputOptions,
      rename: Map[String, String]
  ) extends Action

  case InvokeCompiler(
      compiler: Compiler,
      dependencies: VcpkgDependencies,
      rename: Map[String, String],
      args: Seq[String]
  ) extends Action

  case InvokeScalaCLI(
      dependencies: VcpkgDependencies,
      rename: Map[String, String],
      args: Seq[String]
  ) extends Action

  case Pass(args: Seq[String])

  case Bootstrap
end Action

case class SuspendedAction(apply: Seq[String] => Action)
object SuspendedAction:
  def immediate(action: Action) = SuspendedAction(_ => action)

case class OutputOptions(
    compile: Boolean,
    linking: Boolean
)

object Options extends VcpkgPluginImpl:
  private val name = "sn-vcpkg"

  private val header = """
  |Bootstraps and installs vcpkg dependencies in a way compatible with 
  |the build tool plugins for SBT or Mill
  """.stripMargin.trim

  private val vcpkgAllowBootstrap =
    Opts
      .flag(
        "no-bootstrap",
        visibility = Visibility.Normal,
        help = "Allow bootstrapping vcpkg from scratch"
      )
      .orTrue

  private val envInit = Opts
    .option[String](
      "vcpkg-root-env",
      metavar = "env-var",
      visibility = Visibility.Normal,
      help = "Pick up vcpkg root from the environment variable"
    )
    .product(vcpkgAllowBootstrap)
    .map[VcpkgRootInit](VcpkgRootInit.FromEnv(_, _))

  private val manualInit = Opts
    .option[String](
      "vcpkg-root-manual",
      metavar = "location",
      visibility = Visibility.Normal,
      help = "Initialise vcpkg in this location"
    )
    .map(fname => new java.io.File(fname))
    .product(vcpkgAllowBootstrap)
    .map[VcpkgRootInit](VcpkgRootInit.Manual(_, _))

  private val vcpkgRootInit = manualInit
    .orElse(envInit)
    .orElse(
      vcpkgAllowBootstrap
        .map[VcpkgRootInit](allow => VcpkgRootInit.SystemCache(allow))
    )

  private val vcpkgInstallDir = Opts
    .option[String](
      "vcpkg-install",
      metavar = "dir",
      help = "folder where packages will be installed"
    )
    .map(new File(_))
    .withDefault(defaultInstallDir)

  private val rename = Opts
    .option[String](
      "rename",
      metavar = "spec1,spec2,spec3",
      help =
        "rename packages when looking up their flags in pkg-config\ne.g. --rename curl=libcurl,cjson=libcjson"
    )
    .map: specs =>
      specs
        .split(',')
        .map: spec =>
          val List(from, to) = spec.split("=", 2).toList
          from -> to
        .toMap
    .withDefault(Map.empty)

  private val verbose = Opts
    .flag(
      long = "verbose",
      short = "v",
      visibility = Visibility.Normal,
      help = "Verbose logging"
    )
    .orFalse

  private val quiet = Opts
    .flag(
      long = "quiet",
      short = "q",
      visibility = Visibility.Normal,
      help = "Only error logging"
    )
    .orFalse

  private val outputCompile = Opts
    .flag(
      "output-compilation",
      short = "c",
      help =
        "Output (to STDOUT) compilation flags for installed libraries, one per line"
    )
    .orFalse

  private val outputLinking = Opts
    .flag(
      "output-linking",
      short = "l",
      help =
        "Output (to STDOUT) linking flags for installed libraries, one per line"
    )
    .orFalse

  private val out =
    (outputCompile, outputLinking).mapN(OutputOptions.apply)

  private val deps =
    Opts
      .option[String](
        "manifest",
        help = "vcpkg manifest file"
      )
      .map(new File(_))
      .validate("File should exist")(f => f.exists() && f.isFile())
      .map(VcpkgDependencies.apply)
      .orElse(
        Opts
          .arguments[String](metavar = "dep")
          .map(_.toList)
          .map(deps => VcpkgDependencies.apply(deps*))
      )

  private val actionInstall = (deps, out, rename).mapN(Action.Install.apply)

  val logger = ExternalLogger(
    debug = scribe.debug(_),
    info = scribe.info(_),
    warn = scribe.warn(_),
    error = scribe.error(_)
  )

  case class Config(
      rootInit: VcpkgRootInit,
      installDir: File,
      allowBootstrap: Boolean,
      verbose: Boolean,
      quiet: Boolean
  )

  private val configOpts =
    (vcpkgRootInit, vcpkgInstallDir, vcpkgAllowBootstrap, verbose, quiet).mapN(
      Config.apply
    )

  private val argsOpt =
    Opts
      .arguments[String](metavar = "arg")
      .map(_.toList.toSeq)

  private val install =
    Opts.subcommand("install", "Install a list of vcpkg dependencies")(
      (actionInstall.map(SuspendedAction.immediate), configOpts).tupled
    )

  private val bootstrap =
    Opts.subcommand("bootstrap", "Bootstrap vcpkg")(
      configOpts.map(SuspendedAction.immediate(Action.Bootstrap) -> _)
    )

  private def compilerCommand(compiler: Compiler) =
    (deps, rename).tupled.map((d, r) =>
      SuspendedAction(rest => Action.InvokeCompiler(compiler, d, r, rest))
    )

  private val scalaCliCommand =
    (deps, rename).tupled.map((d, r) =>
      SuspendedAction(rest => Action.InvokeScalaCLI(d, r, rest))
    )

  private val passCommand =
    Opts(SuspendedAction(rest => Action.Pass(rest)))

  private val pass =
    Opts.subcommand("pass", "Invoke vcpkg CLI with passed arguments")(
      (
        passCommand,
        configOpts
      ).tupled
    )

  private val clang =
    Opts.subcommand(
      "clang",
      "Invoke clang with the correct flags for passed dependencies" +
        "The format of the command is [sn-vcpkg clang   -- "
    )(
      (compilerCommand(Compiler.Clang), configOpts).tupled
    )
  private val clangPP =
    Opts.subcommand(
      "clang++",
      "Invoke clang++ with the correct flags for passed dependencies. \n" +
        "The format of the command is [sn-vcpkg clang++   -- "
    )(
      (compilerCommand(Compiler.ClangPP), configOpts).tupled
    )
  private val scalaCli =
    Opts.subcommand(
      "scala-cli",
      "Invoke Scala CLI with correct flags for passed dependencies. Sets --native automatically!" +
        "The format of the command is [sn-vcpkg scala-cli   -- "
    )(
      (scalaCliCommand, configOpts).tupled
    )

  val opts =
    Command(name, header)(
      install orElse bootstrap orElse clang orElse clangPP orElse scalaCli orElse pass
    )

end Options

object C:
  given cdc1: Decoder[VcpkgManifestDependency] =
    Decoder.instance { curs =>
      val name     = curs.get[String]("name")
      val features = curs.getOrElse[List[String]]("features")(Nil)

      import cats.syntax.all.*

      (name, features).mapN(VcpkgManifestDependency.apply)

    }

  given Decoder[Either[String, VcpkgManifestDependency]] =
    Decoder.decodeString
      .map(Left(_))
      .or(summon[Decoder[VcpkgManifestDependency]].map(Right(_)))

  given Decoder[VcpkgManifestFile] =
    Decoder.instance { curs =>
      val name = curs.get[String]("name")
      val deps = curs.getOrElse[List[Either[String, VcpkgManifestDependency]]](
        "dependencies"
      )(Nil)

      import cats.syntax.all.*

      (name, deps).mapN(VcpkgManifestFile.apply)

    }
end C

enum NativeFlag:
  case Compilation(value: String)
  case Linking(value: String)

  def get: String = this match
    case Compilation(value) => value
    case Linking(value)     => value

object VcpkgCLI extends VcpkgPluginImpl, VcpkgPluginNativeImpl:
  import Options.*

  def main(args: Array[String]): Unit =
    scribe.Logger.root
      .clearHandlers()
      .withHandler(writer = scribe.writer.SystemErrWriter)
      .replace()
    val arguments = args.takeWhile(_ != "--")
    val rest      = args.drop(arguments.length + 1)
    opts.parse(arguments) match
      case Left(help) =>
        val (modified, code) =
          if help.errors.nonEmpty then help.copy(body = Nil) -> -1
          else help                                          -> 0
        System.err.println(modified)
        if code != 0 then sys.exit(code)
      case Right((suspended, config)) =>
        val action = suspended.apply(rest)
        import config.*
        if verbose then
          scribe.Logger.root.withMinimumLevel(scribe.Level.Trace).replace()

        if quiet then scribe.Logger.root.clearHandlers().replace()

        val root = rootInit.locate(logger).fold(sys.error(_), identity)
        scribe.debug(s"Locating/bootstrapping vcpkg in ${root.file}")

        val binary = vcpkgBinaryImpl(root, logger)
        scribe.debug(s"Binary is $binary")

        val manager = VcpkgBootstrap.manager(binary, installDir, logger)
        def configurator(info: Map[Dependency, FilesInfo]) =
          VcpkgConfigurator(manager.config, info, logger)

        def summary(mp: Seq[Dependency]) =
          mp.map(_.name).toList.sorted.mkString(", ")

        def computeNativeFlags(
            opt: OutputOptions,
            deps: List[Dependency],
            info: Map[Dependency, FilesInfo],
            rename: Map[String, String]
        ): List[NativeFlag] =
          val flags = List.newBuilder[NativeFlag]

          if opt.compile then
            flags ++= compilationFlags(
              configurator(info),
              deps.map(_.short),
              logger,
              VcpkgNativeConfig().withRenamedLibraries(rename)
            ).map(NativeFlag.Compilation(_))

          if opt.linking then
            flags ++= linkingFlags(
              configurator(info),
              deps.map(_.short),
              logger,
              VcpkgNativeConfig().withRenamedLibraries(rename)
            ).map(NativeFlag.Linking(_))

          flags.result()
        end computeNativeFlags

        action match
          case Action.Pass(args) =>
            vcpkgPassImpl(args, manager, logger).foreach(println)
          case Action.Bootstrap =>
            defaultRootInit.locate(logger) match
              case Left(value) => scribe.error(value)
              case Right(value) =>
                val bin = vcpkgBinaryImpl(value, logger)
                scribe.info(s"Vcpkg bootstrapped in $bin")
          case Action.Install(deps, output, rename) =>
            val result = vcpkgInstallImpl(deps, manager, logger)

            import io.circe.parser.decode
            import C.given

            val allDeps =
              deps.dependencies(str =>
                decode[VcpkgManifestFile](str).fold(throw _, identity)
              )

            computeNativeFlags(
              output,
              allDeps,
              result.filter((k, v) => allDeps.contains(k)),
              rename
            ).map(_.get).foreach(println)
          case Action.InvokeCompiler(compiler, deps, rename, rest) =>
            val result = vcpkgInstallImpl(deps, manager, logger)
            import io.circe.parser.decode
            import C.given
            val allDeps =
              deps.dependencies(str =>
                decode[VcpkgManifestFile](str).fold(throw _, identity)
              )
            val compilerArgs = List.newBuilder[String]

            compilerArgs += (compiler match
              case Compiler.Clang   => "clang"
              case Compiler.ClangPP => "clang++"
            )

            compilerArgs.addAll(rest)

            computeNativeFlags(
              OutputOptions(compile = true, linking = true),
              allDeps,
              result.filter((k, v) => allDeps.contains(k)),
              rename
            ).map(_.get).foreach(compilerArgs.addOne)

            scribe.debug(
              s"Invoking ${compiler} with arguments: [${compilerArgs.result().mkString(" ")}]"
            )

            val exitCode = scala.sys.process.Process(compilerArgs.result()).!

            sys.exit(exitCode)

          case Action.InvokeScalaCLI(deps, rename, rest) =>
            val result = vcpkgInstallImpl(deps, manager, logger)
            import io.circe.parser.decode
            import C.given
            val allDeps =
              deps.dependencies(str =>
                decode[VcpkgManifestFile](str).fold(throw _, identity)
              )
            val scalaCliBin = Platform.os match
              case Platform.OS.Windows => "scala-cli.bat"
              case _                   => "scala-cli"
            val scalaCliArgs = List.newBuilder[String]

            scalaCliArgs.addOne(scalaCliBin)
            scalaCliArgs.addAll(rest)

            computeNativeFlags(
              OutputOptions(compile = true, linking = true),
              allDeps,
              result.filter((k, v) => allDeps.contains(k)),
              rename
            ).foreach {
              case NativeFlag.Compilation(value) =>
                scalaCliArgs.addOne("--native-compile")
                scalaCliArgs.addOne(value)

              case NativeFlag.Linking(value) =>
                scalaCliArgs.addOne("--native-linking")
                scalaCliArgs.addOne(value)
            }

            scribe.debug(
              s"Invoking Scala CLI with arguments: [${scalaCliArgs.result().mkString(" ")}]"
            )

            val exitCode = scala.sys.process.Process(scalaCliArgs.result()).!

            sys.exit(exitCode)

        end match
    end match
  end main

end VcpkgCLI




© 2015 - 2025 Weber Informatics LLC | Privacy Policy