From 4b7f822f632c7382d5625d5281ab3e87f9636bbf Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Mon, 21 Nov 2022 13:44:07 +0000 Subject: [PATCH] Manifest and cli (#72) * CLI module and manifest file support --- .github/workflows/ci.yml | 16 +- .scalafmt.conf | 12 +- build.sbt | 41 ++++- cli/src/main/scala/CommandLineApp.scala | 163 ++++++++++++++++++ .../scala/com/indoorvivants/vcpkg/Vcpkg.scala | 10 +- .../indoorvivants/vcpkg/VcpkgManifest.scala | 1 + .../indoorvivants/vcpkg/VcpkgPluginImpl.scala | 33 +++- .../vcpkg/mill/VcpkgModule.scala | 15 +- .../vcpkg/mill/VcpkgModuleSpec.scala | 9 +- mill-plugin/src/test/vcpkg.json | 8 + .../indoorvivants/vcpkg/sbt/VcpkgPlugin.scala | 20 ++- .../src/sbt-test/sbt-vcpkg/simple/build.sbt | 4 +- sbt-plugin/src/sbt-test/sbt-vcpkg/simple/test | 1 + .../src/sbt-test/sbt-vcpkg/simple/vcpkg.json | 9 + 14 files changed, 318 insertions(+), 24 deletions(-) create mode 100644 cli/src/main/scala/CommandLineApp.scala create mode 100644 core/src/main/scala/com/indoorvivants/vcpkg/VcpkgManifest.scala create mode 100644 mill-plugin/src/test/vcpkg.json create mode 100644 sbt-plugin/src/sbt-test/sbt-vcpkg/simple/vcpkg.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d64a6d..bc00d56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,19 @@ jobs: cd example SBT_VCPKG_VERSION=$(cat ../version) sbt example/run + - name: CLI tests + shell: bash + run: | + set -e + + curl -fLo cs https://github.com/coursier/launchers/raw/master/coursier && + chmod +x cs + + ./cs launch com.indoorvivants.vcpkg:scala-vcpkg_3:$(cat version) -- install -v libpq s2n + echo '{"name": "my-application","version": "0.15.2","dependencies": ["sqlite3"]}' > test-vcpkg.json + ./cs launch com.indoorvivants.vcpkg:scala-vcpkg_3:$(cat version) -- install-manifest -v test-vcpkg.json + windows_build: name: Windows CI @@ -79,10 +91,6 @@ jobs: - name: Test run: sbt test - - name: Cold start tests (docker) - run: docker build . -t sbt-vcpkg-tests - if: matrix.os == 'ubuntu-20.04' - summary: name: Build summary runs-on: ubuntu-latest diff --git a/.scalafmt.conf b/.scalafmt.conf index 439185c..1d447b2 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,12 @@ version = "3.6.1" -runner.dialect = scala213 \ No newline at end of file +runner.dialect = scala213 + +fileOverride { + "glob:**/cli/**/*.scala" { + runner.dialect = scala3 + rewrite.scala3.insertEndMarkerMinLines = 10 + rewrite.scala3.removeOptionalBraces = true + rewrite.scala3.convertToNewSyntax = true + align.preset = more + } +} diff --git a/build.sbt b/build.sbt index d147c44..5511a9a 100644 --- a/build.sbt +++ b/build.sbt @@ -41,6 +41,10 @@ val V = new { val b2s = "0.3.17" + val decline = "2.4.0" + + val scribe = "3.10.5" + val supportedScalaVersions = List(scala213, scala212, scala3) } @@ -51,9 +55,10 @@ lazy val publishing = Seq( lazy val root = project .in(file(".")) - .aggregate( - (core.projectRefs ++ `sbt-plugin`.projectRefs ++ `mill-plugin`.projectRefs) * - ) + .aggregate(core.projectRefs *) + .aggregate(`sbt-plugin`.projectRefs *) + .aggregate(`mill-plugin`.projectRefs *) + .aggregate(cli.projectRefs *) .settings( publish / skip := true ) @@ -64,17 +69,31 @@ lazy val core = projectMatrix .settings(publishing) .settings( name := "vcpkg-core", - libraryDependencies += "dev.dirs" % "directories" % V.dirs, - libraryDependencies += "com.indoorvivants.detective" %% "platform" % V.detective, - crossScalaVersions := V.supportedScalaVersions, - libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit" % V.eclipseGit, - libraryDependencies += "com.disneystreaming" %% "weaver-cats" % V.weaver, + libraryDependencies ++= Seq( + "dev.dirs" % "directories" % V.dirs, + "com.indoorvivants.detective" %% "platform" % V.detective, + "org.eclipse.jgit" % "org.eclipse.jgit" % V.eclipseGit, + "com.disneystreaming" %% "weaver-cats" % V.weaver % Test + ), testFrameworks += new TestFramework("weaver.framework.CatsEffect"), scalacOptions ++= { if (!scalaVersion.value.startsWith("3.")) Seq("-Xsource:3") else Seq.empty } ) +lazy val cli = projectMatrix + .jvmPlatform(scalaVersions = Seq(V.scala3)) + .defaultAxes(VirtualAxis.scalaABIVersion(V.scala3), VirtualAxis.jvm) + .dependsOn(core) + .in(file("cli")) + .settings(publishing) + .settings( + name := "scala-vcpkg", + testFrameworks += new TestFramework("weaver.framework.CatsEffect"), + libraryDependencies += "com.monovore" %% "decline" % V.decline, + libraryDependencies += "com.outr" %% "scribe" % V.scribe + ) + lazy val `sbt-plugin` = projectMatrix .jvmPlatform(scalaVersions = Seq(V.scala212)) .in(file("sbt-plugin")) @@ -101,7 +120,11 @@ lazy val `mill-plugin` = projectMatrix name := """mill-vcpkg""", libraryDependencies += "com.lihaoyi" %% "mill-scalalib" % V.mill, libraryDependencies += "com.lihaoyi" %% "utest" % V.utest % Test, - testFrameworks += new TestFramework("utest.runner.Framework") + testFrameworks += new TestFramework("utest.runner.Framework"), + Test / fork := true, + Test / envVars := Map( + "MILL_VCPKG_ROOT" -> ((ThisBuild / baseDirectory).value / "mill-plugin" / "src" / "test").toString + ) ) Global / onChangedBuildSource := ReloadOnSourceChanges diff --git a/cli/src/main/scala/CommandLineApp.scala b/cli/src/main/scala/CommandLineApp.scala new file mode 100644 index 0000000..9bb5891 --- /dev/null +++ b/cli/src/main/scala/CommandLineApp.scala @@ -0,0 +1,163 @@ +package com.indoorvivants.vcpkg +package cli + +import cats.implicits.* +import com.monovore.decline.* +import java.io.File + +enum Action: + case Install(dependencies: Seq[String]) + case InstallManifest(file: File) + +object Options extends VcpkgPluginImpl: + private val name = "scala-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 verbose = Opts + .flag( + long = "verbose", + short = "v", + visibility = Visibility.Normal, + help = "Verbose logging" + ) + .orFalse + + private val actionInstall = + Opts + .arguments[String](metavar = "dep") + .map(_.toList) + .map(Action.Install(_)) + + private val actionInstallManifest = + Opts + .argument[String]( + "vcpkg manifest file" + ) + .map(new File(_)) + .validate("File should exist")(f => f.exists() && f.isFile()) + .map(Action.InstallManifest(_)) + + 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 + ) + + private val configOpts = + (vcpkgRootInit, vcpkgInstallDir, vcpkgAllowBootstrap, verbose).mapN( + Config.apply + ) + + private val install = + Opts.subcommand("install", "Install a list of vcpkg dependencies")( + (actionInstall, configOpts).tupled + ) + + private val installManifest = Opts.subcommand( + "install-manifest", + "Install vcpkg dependencies from a manifest file (like vcpkg.json)" + )( + (actionInstallManifest, configOpts).tupled + ) + + val opts = Command(name, header)(install orElse installManifest) + +end Options + +object VcpkgCLI extends VcpkgPluginImpl: + import Options.* + + def main(args: Array[String]): Unit = + opts.parse(args) match + case Left(help) => + System.err.println(help) + if help.errors.nonEmpty then sys.exit(1) + else sys.exit(0) + case Right((action, config)) => + import config.* + if verbose then + scribe.Logger.root.withMinimumLevel(scribe.Level.Trace).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) + + action match + case Action.Install(dependencies) => + scribe.info( + "Installed dependencies: ", + vcpkgInstallImpl(dependencies.toSet, manager, logger) + .map(_._1.name) + .mkString(", ") + ) + case Action.InstallManifest(file) => + scribe.info( + "Installed dependencies: ", + vcpkgInstallManifestImpl(file, manager, logger) + .map(_._1.name) + .mkString(", ") + ) + end match +end VcpkgCLI diff --git a/core/src/main/scala/com/indoorvivants/vcpkg/Vcpkg.scala b/core/src/main/scala/com/indoorvivants/vcpkg/Vcpkg.scala index 48cfaa6..a90e598 100644 --- a/core/src/main/scala/com/indoorvivants/vcpkg/Vcpkg.scala +++ b/core/src/main/scala/com/indoorvivants/vcpkg/Vcpkg.scala @@ -31,14 +31,14 @@ class Vcpkg( private def cmdSeq(args: Seq[String]) = Seq(binary.toString) ++ args ++ Seq(localArg, rootArg) - private def getLines(args: Seq[String]): Vector[String] = { + private def getLines(args: Seq[String], cwd: File = root): Vector[String] = { import sys.process.Process val logs = Logs.logCollector( out = Set(Logs.Buffer, Logs.Redirect(logger.debug)), err = Set(Logs.Buffer, Logs.Redirect(logger.debug)) ) logger.debug(s"Executing ${args.mkString("[", " ", "]")}") - val p = Process.apply(args, cwd = root).run(logs.logger).exitValue() + val p = Process.apply(args, cwd = cwd).run(logs.logger).exitValue() if (p != 0) { logs.dump(logger.error) @@ -55,6 +55,12 @@ class Vcpkg( def install(name: String): Vector[String] = getLines(cmd("install", name, s"--triplet=$vcpkgTriplet", "--recurse")) + def installManifest(file: File): Vector[String] = + getLines( + cmd("install", s"--triplet=$vcpkgTriplet"), + cwd = file.getParentFile() + ) + def installAll(names: Seq[String]): Vector[String] = getLines( cmdSeq( diff --git a/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgManifest.scala b/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgManifest.scala new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgManifest.scala @@ -0,0 +1 @@ + diff --git a/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgPluginImpl.scala b/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgPluginImpl.scala index 7da3daa..f9d4bdc 100644 --- a/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgPluginImpl.scala +++ b/core/src/main/scala/com/indoorvivants/vcpkg/VcpkgPluginImpl.scala @@ -9,6 +9,7 @@ import scala.util.Try import scala.util.Failure import scala.util.Success import com.indoorvivants.detective.Platform +import java.nio.file.Files /** A bunch of build-tool agnostic functions. The trait can be mixed in SBT's or * Mill's native plugin constructs, which can then delegate to these functions, @@ -60,6 +61,34 @@ trait VcpkgPluginImpl { go(maxAttempts) } + protected def vcpkgInstallManifestImpl( + manifest: File, + manager: Vcpkg, + logger: ExternalLogger + ): Map[Dependency, FilesInfo] = { + val tempDir = Files.createTempDirectory("vcpkg-manifest-install") + val manifestFile = + Files.copy(manifest.toPath, tempDir.resolve("vcpkg.json")) + + logger.debug( + s"Installing dependencies from manifest file $manifest (using a working directory $tempDir)" + ) + + VcpkgPluginImpl.synchronized { + manager.installManifest(manifestFile.toFile) + + InstalledList.parse(manager.list(), logger).deps.toSet + + InstalledList + .parse(manager.list(), logger) + .deps + .map { dep => + dep -> files(dep.name, manager.config) + } + .toMap + } + } + protected def vcpkgInstallImpl( dependencies: Set[String], manager: Vcpkg, @@ -93,7 +122,7 @@ trait VcpkgPluginImpl { val dependenciesToInstall = allActualDependencies.filterNot(allInstalledDependencies.contains(_)) - logger.info( + logger.debug( "Already installed dependencies: " + allInstalledDependencies .map(_.short) .toList @@ -167,4 +196,4 @@ trait VcpkgPluginImpl { } -object VcpkgPluginImpl +object VcpkgPluginImpl extends VcpkgPluginImpl diff --git a/mill-plugin/src/main/scala/com/indoorvivants/vcpkg/mill/VcpkgModule.scala b/mill-plugin/src/main/scala/com/indoorvivants/vcpkg/mill/VcpkgModule.scala index 64151c2..72afae3 100644 --- a/mill-plugin/src/main/scala/com/indoorvivants/vcpkg/mill/VcpkgModule.scala +++ b/mill-plugin/src/main/scala/com/indoorvivants/vcpkg/mill/VcpkgModule.scala @@ -25,6 +25,10 @@ trait VcpkgModule extends mill.define.Module with VcpkgPluginImpl { */ def vcpkgDependencies: T[Set[String]] + /** Path to vcpkg manifest file (i.e. vcpkg.json) + */ + def vcpkgManifest: T[Option[os.Path]] = T { None } + /** Whether to bootstrap vcpkg automatically */ def vcpkgRootInit: Task[vcpkg.VcpkgRootInit] = T.task { defaultRootInit } @@ -81,10 +85,17 @@ trait VcpkgModule extends mill.define.Module with VcpkgPluginImpl { } def vcpkgInstall: T[Map[vcpkg.Dependency, vcpkg.FilesInfo]] = T { + val manager = vcpkgManager() + val logger = millLogger(T.log) + + vcpkgManifest().map { path => + vcpkgInstallManifestImpl(path.toIO, manager, logger) + } + vcpkgInstallImpl( dependencies = vcpkgDependencies(), - manager = vcpkgManager(), - logger = millLogger(T.log) + manager, + logger ) } diff --git a/mill-plugin/src/test/scala/com/indoorvivants/vcpkg/mill/VcpkgModuleSpec.scala b/mill-plugin/src/test/scala/com/indoorvivants/vcpkg/mill/VcpkgModuleSpec.scala index aba8808..5d3f7f5 100644 --- a/mill-plugin/src/test/scala/com/indoorvivants/vcpkg/mill/VcpkgModuleSpec.scala +++ b/mill-plugin/src/test/scala/com/indoorvivants/vcpkg/mill/VcpkgModuleSpec.scala @@ -8,8 +8,14 @@ import com.indoorvivants.vcpkg._ object VcpkgModuleSpec extends utest.TestSuite { + val manifestPath = + sys.env.get("MILL_VCPKG_ROOT").map { p => + os.Path(p) / "vcpkg.json" + } + def tests: Tests = Tests { test("base") { + println(sys.env.get("MILL_VCPKG_ROOT")) object build extends TestUtil.BaseModule { object foo extends VcpkgModule { def vcpkgDependencies = T(Set("cmark")) @@ -25,7 +31,8 @@ object VcpkgModuleSpec extends utest.TestSuite { test("pkg-config") { object build extends TestUtil.BaseModule { object foo extends VcpkgModule { - def vcpkgDependencies = T(Set("cmark", "cjson")) + override def vcpkgManifest = T(manifestPath) + def vcpkgDependencies = T(Set("cmark")) } } diff --git a/mill-plugin/src/test/vcpkg.json b/mill-plugin/src/test/vcpkg.json new file mode 100644 index 0000000..6bcc577 --- /dev/null +++ b/mill-plugin/src/test/vcpkg.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "name": "my-application", + "version": "0.15.2", + "dependencies": [ + "cjson" + ] +} diff --git a/sbt-plugin/src/main/scala/com/indoorvivants/vcpkg/sbt/VcpkgPlugin.scala b/sbt-plugin/src/main/scala/com/indoorvivants/vcpkg/sbt/VcpkgPlugin.scala index 05db2b4..3db4b31 100644 --- a/sbt-plugin/src/main/scala/com/indoorvivants/vcpkg/sbt/VcpkgPlugin.scala +++ b/sbt-plugin/src/main/scala/com/indoorvivants/vcpkg/sbt/VcpkgPlugin.scala @@ -18,6 +18,14 @@ object VcpkgPlugin extends AutoPlugin with vcpkg.VcpkgPluginImpl { object autoImport { val vcpkgDependencies = settingKey[Set[String]]("List of vcpkg dependencies") + + val vcpkgManifest = + settingKey[File]( + "Path to the json file to be used as a vcpkg manifest. " + + "If vcpkgDependencies is set and is non empty, then dependencies from the manifest" + + " are installed first, and then the vcpkgDependencies" + ) + val vcpkgInstall = taskKey[Map[vcpkg.Dependency, vcpkg.FilesInfo]]( "Invoke Vcpkg and attempt to install packages" ) @@ -32,6 +40,7 @@ object VcpkgPlugin extends AutoPlugin with vcpkg.VcpkgPluginImpl { val vcpkgBinary = taskKey[File]("Path to vcpkg binary") val vcpkgManager = taskKey[Vcpkg]("") + val vcpkgConfigurator = taskKey[vcpkg.VcpkgConfigurator]("") } @@ -76,10 +85,17 @@ object VcpkgPlugin extends AutoPlugin with vcpkg.VcpkgPluginImpl { ) }, vcpkgInstall := { + val manager = vcpkgManager.value + val logger = sbtLogger(sLog.value) + + vcpkgManifest.?.value.foreach { file => + vcpkgInstallManifestImpl(file, manager, logger) + } + vcpkgInstallImpl( dependencies = vcpkgDependencies.value, - manager = vcpkgManager.value, - logger = sbtLogger(sLog.value) + manager, + logger ) } ) diff --git a/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/build.sbt b/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/build.sbt index 3f43d6c..611f49c 100644 --- a/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/build.sbt +++ b/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/build.sbt @@ -5,7 +5,9 @@ enablePlugins(VcpkgPlugin) import com.indoorvivants.vcpkg -vcpkgDependencies := Set("cmark", "cjson") + +vcpkgDependencies := Set("cmark") +vcpkgManifest := (ThisBuild / baseDirectory).value / "vcpkg.json" val testPkgConfig = taskKey[Unit]("") diff --git a/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/test b/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/test index 77bad8d..b52859d 100644 --- a/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/test +++ b/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/test @@ -1,2 +1,3 @@ +> debug > testPkgConfig > show vcpkgRoot diff --git a/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/vcpkg.json b/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/vcpkg.json new file mode 100644 index 0000000..3ea2aa6 --- /dev/null +++ b/sbt-plugin/src/sbt-test/sbt-vcpkg/simple/vcpkg.json @@ -0,0 +1,9 @@ + +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "name": "my-application", + "version": "0.15.2", + "dependencies": [ + "cjson" + ] +}