diff --git a/multiversion/src/main/scala/multiversion/BazelUtil.scala b/multiversion/src/main/scala/multiversion/BazelUtil.scala new file mode 100644 index 0000000..6e6f10b --- /dev/null +++ b/multiversion/src/main/scala/multiversion/BazelUtil.scala @@ -0,0 +1,45 @@ +package multiversion + +import java.io.PrintWriter +import java.nio.file.Path + +import geny.ByteData +import moped.cli.Application +import moped.json.Result +import moped.json.ValueResult +import moped.progressbars.InteractiveProgressBar +import moped.progressbars.ProcessRenderer +import multiversion.loggers.ProgressBars +import multiversion.loggers.StaticProgressRenderer + +object BazelUtil { + + /** The path to the root of the package owning the given label. */ + def packageRoot(app: Application, label: String): Result[Path] = { + val command = List( + "query", + label, + "--output", + "package" + ) + + bazel(app, command).map { out => + app.env.workingDirectory.resolve(out.trim()) + } + } + + def bazel(app: Application, command: List[String]): Result[ByteData.Chunks] = { + val pr0 = new ProcessRenderer(command, command, clock = app.env.clock) + val pr = StaticProgressRenderer.ifAnsiDisabled(pr0, app.env.isColorEnabled) + val pb = new InteractiveProgressBar(out = new PrintWriter(app.env.standardError), renderer = pr) + val process = ProgressBars.run(pb) { + os.proc("bazel" :: command) + .call(cwd = os.Path(app.env.workingDirectory), stderr = pr0.output, check = false) + } + if (process.exitCode == 0) { + ValueResult(process.out) + } else { + pr0.asErrorResult(process.exitCode) + } + } +} diff --git a/multiversion/src/main/scala/multiversion/commands/LintCommand.scala b/multiversion/src/main/scala/multiversion/commands/LintCommand.scala index 28c9ddd..ab6a7dd 100644 --- a/multiversion/src/main/scala/multiversion/commands/LintCommand.scala +++ b/multiversion/src/main/scala/multiversion/commands/LintCommand.scala @@ -1,6 +1,5 @@ package multiversion.commands -import java.io.PrintWriter import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path @@ -10,19 +9,16 @@ import scala.collection.JavaConverters._ import com.twitter.multiversion.Build.QueryResult import moped.annotations.CommandName import moped.annotations.Description +import moped.annotations.Flag import moped.annotations.PositionalArguments import moped.cli.Application import moped.cli.Command import moped.cli.CommandParser import moped.json.Result -import moped.json.ValueResult -import moped.progressbars.InteractiveProgressBar -import moped.progressbars.ProcessRenderer +import multiversion.BazelUtil import multiversion.diagnostics.MultidepsEnrichments._ import multiversion.indexes.DependenciesIndex import multiversion.indexes.TargetIndex -import multiversion.loggers.ProgressBars -import multiversion.loggers.StaticProgressRenderer import multiversion.outputs.LintOutput import multiversion.resolvers.SimpleDependency import org.typelevel.paiges.Doc @@ -30,39 +26,21 @@ import org.typelevel.paiges.Doc @CommandName("lint") case class LintCommand( @Description("File to write lint report") lintReportPath: Option[Path] = None, + @Description("Automatically mark failing target as pending") @Flag lintMarkPending: Boolean = + false, @PositionalArguments queryExpressions: List[String] = Nil, app: Application = Application.default ) extends Command { private def runQuery(queryExpression: String): Result[QueryResult] = { val command = List( - "bazel", "query", queryExpression, "--noimplicit_deps", "--notool_deps", "--output=proto" ) - val pr0 = new ProcessRenderer(command, command, clock = app.env.clock) - val pr = StaticProgressRenderer.ifAnsiDisabled( - pr0, - app.env.isColorEnabled - ) - val pb = new InteractiveProgressBar( - out = new PrintWriter(app.env.standardError), - renderer = pr - ) - val process = ProgressBars.run(pb) { - os.proc(command) - .call( - cwd = os.Path(app.env.workingDirectory), - stderr = pr0.output, - check = false - ) - } - if (process.exitCode == 0) { - ValueResult(QueryResult.parseFrom(process.out.bytes)) - } else { - pr0.asErrorResult(process.exitCode) + BazelUtil.bazel(app, command).map { out => + QueryResult.parseFrom(out.bytes) } } @@ -102,14 +80,18 @@ case class LintCommand( !deps.exists(isTransitive) } - LintOutput(root, reportedErrors) + val isFailure = reportedErrors.nonEmpty && !isPending(app, root) + LintOutput(root, reportedErrors, isFailure) } for { - LintOutput(root, errors) <- lintResults + LintOutput(root, errors, isFailure) <- lintResults + log = + if (isFailure) app.reporter.error _ + else (s: String) => app.reporter.warning(s"(Pending) $s") (module, versions) <- errors } { - app.reporter.error( + log( s"target '$root' depends on conflicting versions of the 3rdparty dependency '${module.repr}:{${versions.commas}}'.\n" + s"\tTo fix this problem, modify the dependency list of this target so that it only depends on one version of the 3rdparty module '${module.repr}'" ) @@ -125,6 +107,21 @@ case class LintCommand( } } } + + private def isPending(app: Application, label: String): Boolean = { + BazelUtil + .packageRoot(app, label) + .map { path => + val pendingBazelFile = path.resolve("PENDING.bazel") + val pendingFile = path.resolve("PENDING") + if (Files.isRegularFile(pendingBazelFile) || Files.isRegularFile(pendingFile)) true + else if (lintMarkPending) { + Files.createFile(pendingBazelFile) + true + } else false + } + .getOrElse(false) + } } object LintCommand { diff --git a/multiversion/src/main/scala/multiversion/outputs/Docs.scala b/multiversion/src/main/scala/multiversion/outputs/Docs.scala index 18a53a6..8115eef 100644 --- a/multiversion/src/main/scala/multiversion/outputs/Docs.scala +++ b/multiversion/src/main/scala/multiversion/outputs/Docs.scala @@ -21,6 +21,15 @@ object Docs { val openBracket: Doc = Doc.char('[') val closeBracket: Doc = Doc.char(']') val colon: Doc = Doc.char(':') + def obj(entries: Iterable[(String, Doc)]): Doc = { + val mappings = entries.map { + case (key, value) => + literal(key) + colon + Doc.space + value + } + Doc + .intercalate(Doc.comma + Doc.space, mappings) + .tightBracketBy(openBrace, closeBrace) + } object emoji { val success: Doc = colors.green + Doc.text("✔ ") + colors.reset val error: Doc = Doc.text("❗") diff --git a/multiversion/src/main/scala/multiversion/outputs/LintOutput.scala b/multiversion/src/main/scala/multiversion/outputs/LintOutput.scala index bcaa813..5d7c7bb 100644 --- a/multiversion/src/main/scala/multiversion/outputs/LintOutput.scala +++ b/multiversion/src/main/scala/multiversion/outputs/LintOutput.scala @@ -5,20 +5,25 @@ import org.typelevel.paiges.Doc final case class LintOutput( root: String, - conflicts: Map[SimpleModule, Set[String]] + conflicts: Map[SimpleModule, Set[String]], + isFailure: Boolean ) { def toDoc: Doc = { // Sort the conflicts to ensure the output is stable. val sortedConflicts = conflicts - .map { case (module, versions) => module.repr -> versions.toList.sorted } + .map { + case (module, versions) => + val versionsDoc = Docs.array(versions.toList.sorted: _*) + module.repr -> versionsDoc + } .toList .sortBy(_._1) - val conflictDocs = sortedConflicts.map { - case (module, versions) => - Docs.literal(module) + Docs.colon + Doc.space + Docs.array(versions: _*) - } - Docs.literal(root) + Docs.colon + Doc.space + Doc - .intercalate(Doc.comma + Doc.space, conflictDocs) - .tightBracketBy(Docs.openBrace, Docs.closeBrace) + + Docs.literal(root) + Docs.colon + Doc.space + Docs.obj( + List( + "failure" -> Doc.str(isFailure), + "conflicts" -> Docs.obj(sortedConflicts) + ) + ) } }