Skip to content

Commit 5f3f569

Browse files
committed
basic lockfile support
1 parent b994d24 commit 5f3f569

File tree

9 files changed

+120
-20
lines changed

9 files changed

+120
-20
lines changed

.mobala/steps/run-test.sh

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function run-test() {
88
target/graalvm-native-image/baboon \
99
--model-dir ./src/test/resources/baboon/ \
1010
--meta-write-evolution-json baboon-meta.json \
11+
--lock-file=./target/baboon.lock \
1112
:cs \
1213
--output ./test/cs-stub/BaboonDefinitions/Generated \
1314
--test-output ./test/cs-stub/BaboonTests/GeneratedTests \
@@ -22,25 +23,26 @@ function run-test() {
2223
--fixture-output ./test/sc-stub/src/main/scala/generated-fixtures \
2324
--sc-write-evolution-dict true \
2425
--sc-wrapped-adt-branch-codecs false
25-
26+
2627
pushd .
2728
cd ./test/cs-stub
2829
dotnet build -c Release
2930
dotnet test -c Release BaboonTests/BaboonTests.csproj
3031
popd
31-
32+
3233
pushd .
3334
cd ./test/sc-stub
3435
sbt +clean +test
3536
popd
36-
37+
3738
popd
3839

3940
pushd .
4041

4142
target/graalvm-native-image/baboon \
4243
--model-dir ./src/test/resources/baboon/ \
4344
--meta-write-evolution-json baboon-meta.json \
45+
--lock-file=./target/baboon.lock \
4446
:cs \
4547
--output ./test/cs-stub/BaboonDefinitions/Generated \
4648
--test-output ./test/cs-stub/BaboonTests/GeneratedTests \
@@ -64,37 +66,37 @@ function run-test() {
6466
cd ./test/cs-stub
6567
dotnet build -c Debug
6668
dotnet test -c Debug BaboonTests/BaboonTests.csproj
67-
popd
68-
69+
popd
70+
6971
pushd .
7072
cd ./test/sc-stub
7173
sbt +clean +test
7274
popd
73-
75+
7476
popd
75-
77+
7678
pushd .
77-
rm -rf ./test/conv-test-cs/ConvTest/Generated
79+
rm -rf ./test/conv-test-cs/ConvTest/Generated
7880

7981
# sbt "run --model-dir ./test/conv-test --output ./test/conv-test-cs/ConvTest/Generated"
80-
82+
8183
target/graalvm-native-image/baboon \
8284
--model-dir ./test/conv-test \
8385
:cs \
8486
--output ./test/conv-test-cs/ConvTest/Generated \
8587
:scala \
86-
--output ./test/conv-test-sc/src/main/scala/generated-main
88+
--output ./test/conv-test-sc/src/main/scala/generated-main
8789

8890
pushd .
8991
cd ./test/conv-test-cs
9092
dotnet build
91-
dotnet test
92-
popd
93-
94-
pushd .
93+
dotnet test
94+
popd
95+
96+
pushd .
9597
cd ./test/conv-test-sc
9698
sbt +clean +test
9799
popd
98-
99-
popd
100+
101+
popd
100102
}

src/main/scala/io/septimalmind/baboon/Baboon.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ object Baboon {
8484
directoryInputs = directoryInputs,
8585
targets = launchArgs,
8686
metaWriteEvolutionJsonTo = generalOptions._1.metaWriteEvolutionJson.map(s => Paths.get(s)),
87+
lockFile = generalOptions._1.lockFile.map(s => Paths.get(s)),
8788
)
8889

8990
import izumi.distage.modules.support.unsafe.EitherSupport.{defaultModuleEither, quasiIOEither, quasiIORunnerEither}

src/main/scala/io/septimalmind/baboon/BaboonCompiler.scala

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
11
package io.septimalmind.baboon
22

3+
import io.circe.generic.semiauto.{deriveCodec, deriveEncoder}
4+
import io.circe.{Codec, Decoder, Encoder, KeyDecoder, KeyEncoder}
35
import io.septimalmind.baboon.parser.model.issues.BaboonIssue
4-
import io.septimalmind.baboon.parser.model.issues.BaboonIssue.CantCleanupTarget
6+
import io.septimalmind.baboon.parser.model.issues.BaboonIssue.{CantCleanupTarget, CantReadInput, LockedVersionModified}
57
import io.septimalmind.baboon.translator.{BaboonAbstractTranslator, OutputFile}
6-
import io.septimalmind.baboon.typer.model.BaboonFamily
8+
import io.septimalmind.baboon.typer.BaboonEnquiries
9+
import io.septimalmind.baboon.typer.model.{BaboonFamily, Domain, Pkg, UnmodifiedSince, Version}
710
import io.septimalmind.baboon.util.{BLogger, BaboonMetagen}
811
import izumi.functional.bio.unsafe.MaybeSuspend2
912
import izumi.functional.bio.{Error2, F}
1013
import izumi.fundamentals.collections.nonempty.NEList
14+
import izumi.fundamentals.platform.crypto.IzSha256HashFunction
1115
import izumi.fundamentals.platform.files.IzFiles
1216

1317
import java.nio.charset.StandardCharsets
1418
import java.nio.file.{Files, Path, StandardOpenOption}
1519

20+
case class SigId(value: String) extends AnyVal
21+
22+
case class VersionLock(
23+
version: Version,
24+
sigId: SigId,
25+
)
26+
27+
case class Locks(
28+
locks: Map[Pkg, List[VersionLock]]
29+
)
30+
31+
object LockCodecs {
32+
implicit lazy val pkgKeyEncoder: KeyEncoder[Pkg] = KeyEncoder.encodeKeyString.contramap(_.toString)
33+
implicit lazy val pkgKeyDecoder: KeyDecoder[Pkg] = KeyDecoder.decodeKeyString.map(s => Pkg(NEList.unsafeFrom(s.split('.').toList)))
34+
implicit lazy val versionCodec: Codec[Version] = Codec.from(Decoder.decodeString.map(s => Version(s)), Encoder.encodeString.contramap(_.version))
35+
implicit lazy val sigidCodec: Codec[SigId] = Codec.from(Decoder.decodeString.map(s => SigId(s)), Encoder.encodeString.contramap(_.value))
36+
implicit lazy val versionLockCodec: Codec[VersionLock] = deriveCodec
37+
implicit lazy val lockscodec: Codec[Locks] = deriveCodec
38+
39+
}
40+
1641
trait BaboonCompiler[F[+_, +_]] {
1742
def run(target: CompilerTarget, model: BaboonFamily): F[NEList[BaboonIssue], Unit]
1843
}
@@ -23,13 +48,15 @@ object BaboonCompiler {
2348
options: CompilerOptions,
2449
logger: BLogger,
2550
metagen: BaboonMetagen,
51+
enq: BaboonEnquiries,
2652
) extends BaboonCompiler[F] {
2753

2854
override def run(target: CompilerTarget, model: BaboonFamily): F[NEList[BaboonIssue], Unit] = {
2955

3056
for {
3157
_ <- cleanupTargetPaths(target.output)
3258
_ <- writeEvolutionJson(target, model)
59+
_ <- validateLock(model)
3360

3461
_ <- F.maybeSuspend(logger.message(s"${target.id}: generating output..."))
3562
translated <- translator.translate(model)
@@ -47,6 +74,62 @@ object BaboonCompiler {
4774
} yield {}
4875
}
4976

77+
// TODO: move this into a separate component
78+
private def validateLock(model: BaboonFamily): F[NEList[BaboonIssue], Unit] = {
79+
import LockCodecs.*
80+
import io.circe.syntax.*
81+
82+
val currentSigs = Locks(model.domains.map {
83+
case (pkg, lineage) =>
84+
val versions = lineage.versions.toSeq.map {
85+
case (ver, dom) =>
86+
VersionLock(ver, sigOf(dom))
87+
}.toList
88+
(pkg, versions)
89+
}.toMap)
90+
91+
options.lockFile match {
92+
case Some(lockfilePath) if lockfilePath.toFile.exists() =>
93+
for {
94+
existingSigs <- (for {
95+
content <- F.maybeSuspend(IzFiles.readString(lockfilePath))
96+
parsed <- F.fromEither(io.circe.parser.parse(content))
97+
out <- F.fromEither(parsed.as[Locks])
98+
} yield {
99+
out
100+
}).catchAll(e => F.fail(NEList(CantReadInput(lockfilePath.toString, e))))
101+
_ <- compareSigs(model, currentSigs, existingSigs)
102+
} yield {}
103+
104+
case Some(lockfilePath) =>
105+
F.maybeSuspend(IzFiles.writeUtfString(lockfilePath, currentSigs.asJson.spaces2))
106+
case None =>
107+
F.unit
108+
}
109+
}
110+
111+
private def compareSigs(model: BaboonFamily, currentSigs: Locks, existingSigs: Locks): F[NEList[BaboonIssue], Unit] = {
112+
val ci = index(currentSigs)
113+
val ei = index(existingSigs)
114+
val different = ci.filter { case (k, id) => ei.contains(k) && !ei.get(k).contains(id) }.filterNot { case ((pkg, v), _) => model.domains(pkg).evolution.latest == v }
115+
F.ifThenFail(different.nonEmpty)(NEList.unsafeFrom(different.map { case ((pkg, v), _) => LockedVersionModified(pkg, v) }.toList))
116+
}
117+
118+
private def index(currentSigs: Locks): Map[(Pkg, Version), SigId] = {
119+
currentSigs.locks.flatMap {
120+
case (p, v) =>
121+
v.map(v => ((p, v.version), v.sigId))
122+
}.toMap
123+
}
124+
125+
private def sigOf(dom: Domain): SigId = {
126+
val repr = dom.typeMeta.view.toSeq.map {
127+
case (id, meta) =>
128+
s"[${enq.wrap(id)}:${meta.deepId.id}]"
129+
}.mkString("\n")
130+
SigId(IzSha256HashFunction.hash(repr))
131+
}
132+
50133
private def writeFile(content: OutputFile, tgt: Path): F[NEList[BaboonIssue], Unit] = {
51134
F.fromAttempt {
52135
tgt.getParent.toFile.mkdirs()

src/main/scala/io/septimalmind/baboon/CLIOptions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ case class ScCLIOptions(
8383
case class CLIOptions(
8484
@HelpMessage("A list of *.baboon files to process (can be combined with --model-dir)")
8585
model: List[String],
86+
@HelpMessage("A file used to track model signatures")
87+
lockFile: Option[String],
8688
@HelpMessage("A directory to recursively read all the *.baboon files from")
8789
modelDir: List[String],
8890
@HelpMessage("Produces additional debug messages. Do not use.")

src/main/scala/io/septimalmind/baboon/CompilerOptions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ final case class OutputOptions(
101101
final case class CompilerOptions(
102102
individualInputs: Set[Path],
103103
directoryInputs: Set[Path],
104+
lockFile: Option[Path],
104105
debug: Boolean,
105106
targets: Seq[CompilerTarget],
106107
metaWriteEvolutionJsonTo: Option[Path],

src/main/scala/io/septimalmind/baboon/parser/model/issues/BaboonIssue.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ object BaboonIssue {
153153
//
154154
sealed trait VerificationIssue extends BaboonIssue
155155

156+
case class LockedVersionModified(pkg: Pkg, version: Version) extends VerificationIssue
157+
156158
case class MissingTypeDef(domain: Domain, missing: Set[TypeId]) extends VerificationIssue
157159

158160
case class ReferentialCyclesFound(domain: Domain, loops: Set[LoopDetector.Cycles[TypeId]]) extends VerificationIssue

src/main/scala/io/septimalmind/baboon/parser/model/issues/IssuePrinter.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,11 @@ object IssuePrinter {
468468
|""".stripMargin
469469
}
470470

471+
implicit val lockedVersionModified: IssuePrinter[LockedVersionModified] =
472+
(issue: LockedVersionModified) => {
473+
s"""Model ${issue.pkg.toString}@${issue.version} was modified but it's not the latest version so it's locked with the lockfile""".stripMargin
474+
}
475+
471476
implicit val referentialCyclesFoundPrinter: IssuePrinter[ReferentialCyclesFound] =
472477
(issue: ReferentialCyclesFound) => {
473478
val stringLoops = issue.loops.toList
@@ -729,6 +734,7 @@ object IssuePrinter {
729734
}
730735

731736
implicit val verificationIssuePrinter: IssuePrinter[VerificationIssue] = {
737+
case i: LockedVersionModified => apply[LockedVersionModified].stringify(i)
732738
case i: MissingTypeDef => apply[MissingTypeDef].stringify(i)
733739
case i: ReferentialCyclesFound => apply[ReferentialCyclesFound].stringify(i)
734740
case i: IncorrectRootFound => apply[IncorrectRootFound].stringify(i)

src/main/scala/io/septimalmind/baboon/typer/BaboonTyper.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import izumi.fundamentals.collections.nonempty.NEList
1010
import izumi.fundamentals.graphs.struct.IncidenceMatrix
1111
import izumi.fundamentals.graphs.tools.{Toposort, ToposortLoopBreaker}
1212
import izumi.fundamentals.graphs.{DG, GraphMeta}
13+
import izumi.fundamentals.platform.crypto.{IzHash, IzSha256HashFunction}
1314

1415
import scala.annotation.tailrec
1516
import scala.collection.mutable
@@ -208,9 +209,10 @@ object BaboonTyper {
208209
): F[NEList[BaboonIssue.TyperIssue], DeepSchemaId] = {
209210
for {
210211
repr <- F.pure(deepSchemaRepr(id, defs, List.empty))
212+
fullRepr = s"[${enquiries.wrap(id)};${repr
213+
.mkString(",")}]"
211214
} yield {
212-
DeepSchemaId(s"[${enquiries.wrap(id)};${repr
213-
.mkString(",")}]")
215+
DeepSchemaId(IzSha256HashFunction.hash(fullRepr))
214216
}
215217
}
216218

src/test/scala/io/septimalmind/baboon/tests/BaboonTest.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ abstract class BaboonTest[F[+_, +_]: TagKK: BaboonTestModule] extends Spec2[F]()
2323
individualInputs = Set.empty,
2424
directoryInputs = Set(Paths.get("./src/test/resources/baboon")),
2525
metaWriteEvolutionJsonTo = None,
26+
lockFile = Some(Paths.get("./target/baboon.lock")),
2627
targets = Seq(
2728
CSTarget(
2829
id = "C#",

0 commit comments

Comments
 (0)