|
| 1 | +package dev.guardrail.sbt |
| 2 | + |
| 3 | +import sbt._ |
| 4 | +import sbt.Keys._ |
| 5 | +import scoverage.ScoverageKeys |
| 6 | +import complete.DefaultParsers._ |
| 7 | +import com.github.sbt.git.SbtGit._ |
| 8 | +import com.github.sbt.git.SbtGit.GitKeys.gitReader |
| 9 | +import wartremover.WartRemover.autoImport._ |
| 10 | +import scalafix.sbt.ScalafixPlugin.autoImport._ |
| 11 | +import xerial.sbt.Sonatype.autoImport._ |
| 12 | +import sbtversionpolicy.SbtVersionPolicyPlugin.autoImport._ |
| 13 | + |
| 14 | +object Build { |
| 15 | + val stableVersion: SettingKey[String] = SettingKey("stable-version") |
| 16 | + |
| 17 | + def useStableVersions(moduleName: String): Boolean = { |
| 18 | + // NB: Currently, any time any PR that breaks semver is merged, it breaks |
| 19 | + // the build until the next release. |
| 20 | + // |
| 21 | + // A hack here is to just disable semver checking on master, since |
| 22 | + // we already gate semver both at PR time as well as later on |
| 23 | + // during release, so it actually serves no useful purpose to fail |
| 24 | + // master as well. |
| 25 | + val isMasterBranch = sys.env.get("GITHUB_REF").contains("refs/heads/master") |
| 26 | + val isRelease = sys.env.contains("GUARDRAIL_RELEASE_MODULE") |
| 27 | + val isCi = sys.env.contains("GUARDRAIL_CI") |
| 28 | + if (isCi || isRelease) { |
| 29 | + val ignoreBincompat = { |
| 30 | + import scala.sys.process._ |
| 31 | + Seq("support/current-pr-labels.sh", moduleName) |
| 32 | + .lineStream_! |
| 33 | + .exists(Set("major", "minor").contains) |
| 34 | + } |
| 35 | + |
| 36 | + val useStableVersions = !isMasterBranch && (isRelease || !ignoreBincompat) |
| 37 | + println(s"isMasterBranch=${isMasterBranch}, isRelease=${isRelease}, ignoreBincompat=${ignoreBincompat}: useStableVersions=${useStableVersions}") |
| 38 | + if (useStableVersions) { |
| 39 | + println(s" Ensuring bincompat via MiMa during ${sys.env.get("GITHUB_REF")}") |
| 40 | + } else { |
| 41 | + println(s" Skipping bincompat check on ${sys.env.get("GITHUB_REF")}") |
| 42 | + } |
| 43 | + |
| 44 | + useStableVersions |
| 45 | + } else false |
| 46 | + } |
| 47 | + |
| 48 | + def buildSampleProject(name: String, extraLibraryDependencies: Seq[sbt.librarymanagement.ModuleID]) = |
| 49 | + Project(s"sample-${name}", file(s"modules/sample-${name}")) |
| 50 | + .settings(commonSettings) |
| 51 | + .settings(codegenSettings) |
| 52 | + .settings(libraryDependencies += "org.scala-lang.modules" %% "scala-collection-compat" % "2.11.0") |
| 53 | + .settings( |
| 54 | + libraryDependencies ++= extraLibraryDependencies, |
| 55 | + Compile / unmanagedSourceDirectories += baseDirectory.value / "target" / "generated", |
| 56 | + publish / skip := true, |
| 57 | + ) |
| 58 | + |
| 59 | + val excludedWarts = Set(Wart.DefaultArguments, Wart.Product, Wart.Serializable, Wart.Any, Wart.StringPlusAny) |
| 60 | + |
| 61 | + val codegenSettings = Seq( |
| 62 | + ScoverageKeys.coverageExcludedPackages := "<empty>;dev.guardrail.terms.*;dev.guardrail.protocol.terms.*", |
| 63 | + Compile / compile / wartremoverWarnings ++= Warts.unsafe.filterNot(w => excludedWarts.exists(_.clazz == w.clazz)), |
| 64 | + ) |
| 65 | + |
| 66 | + def ifScalaVersion[A](minorPred: Int => Boolean = _ => true)(value: List[A]): Def.Initialize[Seq[A]] = Def.setting { |
| 67 | + scalaVersion.value.split('.') match { |
| 68 | + case Array("2", minor, bugfix) if minorPred(minor.toInt) => value |
| 69 | + case _ => Nil |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + def customTagToVersionNumber(moduleSegment: String, isRelease: Boolean): String => Option[String] = { v => |
| 74 | + val prefix = s"${moduleSegment}-v" |
| 75 | + val stripPrefix: String => String = _.stripPrefix(prefix) |
| 76 | + val stripSuffix: String => String = if (isRelease) { |
| 77 | + _.replaceAll("-[0-9]+-[0-9a-z]+$", "") |
| 78 | + } else identity _ |
| 79 | + if (v.startsWith(prefix)) { Some(stripSuffix(stripPrefix(v))) } |
| 80 | + else { None } |
| 81 | + } |
| 82 | + |
| 83 | + val commonSettings = Seq( |
| 84 | + organization := "dev.guardrail", |
| 85 | + licenses += ("MIT", url("http://opensource.org/licenses/MIT")), |
| 86 | + |
| 87 | + crossScalaVersions := Seq("2.12.18", "2.13.12"), |
| 88 | + scalaVersion := "2.12.18", |
| 89 | + |
| 90 | + // early-semver was a mistake. We already have early-semver guaratees during CI, but including this in the publishing POM |
| 91 | + // ensures that independent 0.x versions are incompatible, even though we know they are. |
| 92 | + versionScheme := None, |
| 93 | + |
| 94 | + scalacOptions ++= Seq( |
| 95 | + "-Xfatal-warnings", |
| 96 | + "-Ydelambdafy:method", |
| 97 | + "-Yrangepos", |
| 98 | + // "-Ywarn-unused-import", // TODO: Enable this! https://github.com/guardrail-dev/guardrail/pull/282 |
| 99 | + "-feature", |
| 100 | + "-unchecked", |
| 101 | + "-deprecation", |
| 102 | + "-encoding", |
| 103 | + "utf8" |
| 104 | + ), |
| 105 | + Test / scalacOptions -= "-Xfatal-warnings", |
| 106 | + Compile / console / scalacOptions -= "-Xfatal-warnings", |
| 107 | + Compile / consoleQuick / scalacOptions -= "-Xfatal-warnings", |
| 108 | + scalacOptions ++= ifScalaVersion(_ <= 11)(List("-Xexperimental")).value, |
| 109 | + scalacOptions ++= ifScalaVersion(_ == 12)(List("-Ypartial-unification")).value, |
| 110 | + Test / parallelExecution := true, |
| 111 | + addCompilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full), |
| 112 | + addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), |
| 113 | + addCompilerPlugin(scalafixSemanticdb), |
| 114 | + sonatypeCredentialHost := "s01.oss.sonatype.org", |
| 115 | + ) |
| 116 | + |
| 117 | + def commonModule(moduleSegment: String) = |
| 118 | + baseModule(s"guardrail-${moduleSegment}", moduleSegment, file(s"modules/${moduleSegment}")) |
| 119 | + |
| 120 | + def baseModule(moduleName: String, moduleSegment: String, path: File): Project = |
| 121 | + Project(id=moduleName, base=path) |
| 122 | + .settings(versionWithGit) |
| 123 | + .settings(useJGit) |
| 124 | + .settings( |
| 125 | + // None of this stuff can be used because of scoping issues. Everything needs to be inlined to avoid just bubbling up to a singleton, since the keys (scopes?) are only valid at the root, not scoped per project. |
| 126 | + // git.gitDescribePatterns := Seq(s"${moduleSegment}-v*"), |
| 127 | + // git.gitDescribedVersion := gitReader.value.withGit(_.describedVersion(gitDescribePatterns.value)).map(v => customTagToVersionNumber(moduleSegment)(v).getOrElse(v)), |
| 128 | + git.useGitDescribe := true, |
| 129 | + version := { |
| 130 | + val isRelease = sys.env.contains("GUARDRAIL_RELEASE_MODULE") |
| 131 | + val overrideVersion = |
| 132 | + git.overrideVersion(git.versionProperty.value) |
| 133 | + val uncommittedSuffix = |
| 134 | + git.makeUncommittedSignifierSuffix(git.gitUncommittedChanges.value, git.uncommittedSignifier.value) |
| 135 | + val releaseVersion = |
| 136 | + git.releaseVersion(git.gitCurrentTags.value, customTagToVersionNumber(moduleSegment, isRelease), uncommittedSuffix) |
| 137 | + val customGitDescribedVersion = gitReader.value.withGit(_.describedVersion(Seq(s"${moduleSegment}-v*"))).map(v => customTagToVersionNumber(moduleSegment, isRelease)(v).getOrElse(v)) |
| 138 | + val describedVersion = |
| 139 | + git.flaggedOptional(git.useGitDescribe.value, git.describeVersion(customGitDescribedVersion, uncommittedSuffix)) |
| 140 | + val datedVersion = git.formattedDateVersion.value |
| 141 | + val commitVersion = git.formattedShaVersion.value |
| 142 | + //Now we fall through the potential version numbers... |
| 143 | + git.makeVersion(Seq( |
| 144 | + overrideVersion, |
| 145 | + releaseVersion, |
| 146 | + describedVersion, |
| 147 | + commitVersion |
| 148 | + )) getOrElse datedVersion // For when git isn't there at all. |
| 149 | + }, |
| 150 | + stableVersion := { |
| 151 | + // Pull the tag(s) matching the tag scheme, defaulting to 0.0.0 |
| 152 | + // for newly created modules that have never been released before |
| 153 | + // (depending on unreleased modules is an error, so this is fine) |
| 154 | + gitReader |
| 155 | + .value |
| 156 | + .withGit(_.describedVersion(Seq(s"${moduleSegment}-v*"))) |
| 157 | + .fold("0.0.0")(v => customTagToVersionNumber(moduleSegment, true)(v).getOrElse(v)) |
| 158 | + } |
| 159 | + ) |
| 160 | + .settings(commonSettings) |
| 161 | + .settings(versionPolicyIntention := { |
| 162 | + val isRelease = sys.env.contains("GUARDRAIL_RELEASE_MODULE") |
| 163 | + if (isRelease) Compatibility.BinaryCompatible else Compatibility.None |
| 164 | + }) |
| 165 | + .settings(name := moduleName) |
| 166 | + .settings(codegenSettings) |
| 167 | + .settings(libraryDependencies ++= Dependencies.testDependencies) |
| 168 | + .settings( |
| 169 | + scalacOptions ++= List( |
| 170 | + "-language:higherKinds", |
| 171 | + "-Xlint:_,-missing-interpolator" |
| 172 | + ), |
| 173 | + description := "Principled code generation for Scala services from OpenAPI specifications", |
| 174 | + homepage := Some(url("https://github.com/guardrail-dev/guardrail")), |
| 175 | + scmInfo := Some( |
| 176 | + ScmInfo( |
| 177 | + url("https://github.com/guardrail-dev/guardrail"), |
| 178 | + "scm:[email protected]:guardrail-dev/guardrail.git" |
| 179 | + ) |
| 180 | + ), |
| 181 | + developers := List( |
| 182 | + Developer( |
| 183 | + id = "blast_hardcheese", |
| 184 | + name = "Devon Stewart", |
| 185 | + |
| 186 | + url = url("http://hardchee.se/") |
| 187 | + ) |
| 188 | + ) |
| 189 | + ) |
| 190 | + .settings( |
| 191 | + scalacOptions ++= ifScalaVersion(_ <= 11)(List("-Xlint:-missing-interpolator,_")).value, |
| 192 | + scalacOptions ++= ifScalaVersion(_ >= 12)(List("-Xlint:-unused,-missing-interpolator,_")).value, |
| 193 | + scalacOptions ++= ifScalaVersion(_ == 12)(List("-Ypartial-unification", "-Ywarn-unused-import")).value, |
| 194 | + scalacOptions ++= ifScalaVersion(_ >= 13)(List("-Ywarn-unused:imports")).value, |
| 195 | + ) |
| 196 | + |
| 197 | + implicit class ProjectSyntax(project: Project) { |
| 198 | + // Adding these to I _think_ work around https://github.com/sbt/sbt/issues/3733 ? |
| 199 | + // Seems like there's probably a better way to do this, but I don't know what it is |
| 200 | + // The intent is we should be able to use `dependsOn(core % Provided)` to compile |
| 201 | + // against the module's classes, but then emit <scope>provided</scope> in the published POM. |
| 202 | + // |
| 203 | + // Currently, it seems as though `dependsOn(core % Provided)` doesn't expose |
| 204 | + // classes from `core` to the depending module, which means even though we get the desired |
| 205 | + // scope in the pom, it's useless since we can't actually compile the project. |
| 206 | + def accumulateClasspath(other: Project): Project = |
| 207 | + project |
| 208 | + .settings(Compile / unmanagedClasspath := { |
| 209 | + val current = (Compile / unmanagedClasspath).value |
| 210 | + val fromOther = (other / Compile / fullClasspathAsJars).value |
| 211 | + (current ++ fromOther).distinct |
| 212 | + }) |
| 213 | + .settings(Runtime / unmanagedClasspath := { |
| 214 | + val current = (Runtime / unmanagedClasspath).value |
| 215 | + val fromOther = (other / Runtime / fullClasspathAsJars).value |
| 216 | + (current ++ fromOther).distinct |
| 217 | + }) |
| 218 | + .settings(Test / unmanagedClasspath := { |
| 219 | + val current = (Test / unmanagedClasspath).value |
| 220 | + val fromOther = (other / Test / fullClasspathAsJars).value |
| 221 | + (current ++ fromOther).distinct ++ (other / Test / exportedProductJars).value |
| 222 | + }) |
| 223 | + .settings(Default / unmanagedClasspath := { |
| 224 | + val current = (Compile / unmanagedClasspath).value |
| 225 | + val fromOther = (other / Compile / fullClasspathAsJars).value |
| 226 | + (current ++ fromOther).distinct |
| 227 | + }) |
| 228 | + |
| 229 | + def customDependsOn(moduleName: String, other: Project, useProvided: Boolean = false): Project = { |
| 230 | + if (useStableVersions(moduleName)) { |
| 231 | + project |
| 232 | + .settings(libraryDependencySchemes += "dev.guardrail" % other.id % "early-semver") |
| 233 | + .settings(libraryDependencies += { |
| 234 | + val base = "dev.guardrail" %% other.id % (other / stableVersion).value |
| 235 | + if (useProvided) base % Provided else base |
| 236 | + }) |
| 237 | + .settings( |
| 238 | + // dependsOn(other % Test) adds the undesirable dependsOn(other % Compile) as a side-effect. |
| 239 | + // Mirror libraryDependencies and source directory tracking to approximate test dependencies |
| 240 | + Test / libraryDependencies ++= (other / Test / libraryDependencies).value, |
| 241 | + // Add everything from `other`'s test scope to our classpath |
| 242 | + Test / unmanagedJars ++= (other / Test / exportedProductJars).value, |
| 243 | + // Carry along `other`'s exportedProductJars along in ours, so subsequent projects can depend on them |
| 244 | + Test / exportedProductJars := (Test / exportedProductJars).value ++ (other / Test / exportedProductJars).value |
| 245 | + ) |
| 246 | + } else { |
| 247 | + project |
| 248 | + .dependsOn(other) |
| 249 | + .accumulateClasspath(other) |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + def providedDependsOn(moduleName: String, other: Project): Project = |
| 254 | + customDependsOn(moduleName, other, true) |
| 255 | + |
| 256 | + def customDependsOn_(moduleName: String, moduleVersion: String, useProvided: Boolean = false): Project = { |
| 257 | + if (true || useStableVersions(moduleName)) { // TODO: Figure out convenient local-dev story |
| 258 | + project |
| 259 | + .settings(libraryDependencySchemes += "dev.guardrail" % moduleName % "early-semver") |
| 260 | + .settings(libraryDependencies += { |
| 261 | + val base = "dev.guardrail" %% moduleName % moduleVersion |
| 262 | + if (useProvided) base % Provided else base |
| 263 | + }) |
| 264 | + } else { |
| 265 | + ??? |
| 266 | + } |
| 267 | + } |
| 268 | + } |
| 269 | +} |
0 commit comments