Skip to content

Commit 591c9f4

Browse files
Adding build infrastructure
This stuff will get broken out into a common package as it becomes clear what each module needs.
1 parent d1e31f1 commit 591c9f4

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed

build.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name := "guardrail-module-pekko-http"
2+
3+
onLoadMessage := WelcomeMessage.welcomeMessage("0.0.0")

project/src/main/scala/Build.scala

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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+
email = "[email protected]",
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

Comments
 (0)