diff --git a/src/main/scala/ee/hrzn/chryse/ChryseApp.scala b/src/main/scala/ee/hrzn/chryse/ChryseApp.scala index 0cf0fe1..0ff3dd6 100644 --- a/src/main/scala/ee/hrzn/chryse/ChryseApp.scala +++ b/src/main/scala/ee/hrzn/chryse/ChryseApp.scala @@ -79,13 +79,15 @@ abstract class ChryseApp { val setup = new DefaultOParserSetup { override def showUsageOnError = Some(true) } + var terminating = false val config = OParser.runParser(parser, args, ChryseAppConfig(), setup) match { case (result, effects) => OParser.runEffects( effects, new DefaultOEffectSetup { - override def terminate(exitState: Either[String, Unit]): Unit = () + override def terminate(exitState: Either[String, Unit]): Unit = + terminating = true }, ) @@ -95,6 +97,8 @@ abstract class ChryseApp { } } + if (terminating) return + println( s"$name ${getClass().getPackage().getImplementationVersion()} " + s"(Chryse ${ChryseApp.getChrysePackage().getImplementationVersion()})", diff --git a/src/main/scala/ee/hrzn/chryse/tasks/BaseTask.scala b/src/main/scala/ee/hrzn/chryse/tasks/BaseTask.scala index 1037f4c..2295aed 100644 --- a/src/main/scala/ee/hrzn/chryse/tasks/BaseTask.scala +++ b/src/main/scala/ee/hrzn/chryse/tasks/BaseTask.scala @@ -3,6 +3,10 @@ package ee.hrzn.chryse.tasks import ee.hrzn.chryse.ChryseAppStepFailureException import java.io.PrintWriter +import java.nio.file.Files +import java.nio.file.Paths +import java.security.MessageDigest +import java.util.HexFormat import scala.sys.process._ abstract class BaseTask { @@ -14,17 +18,87 @@ abstract class BaseTask { "-strip-debug-info", ) - protected def writeCare(path: String, content: String): Unit = { + protected def writePath(path: String, content: String): Unit = { new PrintWriter(path, "utf-8") { try write(content) finally close() } } - protected def runCare(step: String, cmd: Seq[String]): Unit = { - val result = cmd.! - if (result != 0) { + protected def runCmd(step: String, cmd: Seq[String]) = + runCmds(step, Seq(cmd)) + + protected def runCmds( + step: String, + cmds: Iterable[Seq[String]], + ): Unit = { + cmds.foreach(cmd => println(s"($step) running: $cmd")) + val processes = cmds.map(cmd => (cmd, cmd.run())) + val failed = processes.collect { + case (cmd, proc) if proc.exitValue() != 0 => cmd + } + if (!failed.isEmpty) { + println("the following process(es) failed:") + for { cmd <- failed } println(s" $cmd") throw new ChryseAppStepFailureException(step) } } + + protected def runCu(step: String, cu: CompilationUnit) = + runCus(step, Seq(cu)) + + protected def runCus( + step: String, + cus: Iterable[CompilationUnit], + ): Unit = { + val (skip, run) = cus.partition(_.isUpToDate()) + skip.foreach(cu => println(s"($step) skipping: ${cu.cmd}")) + runCmds(step, run.map(_.cmd)) + run.foreach(_.markUpToDate()) + } + + case class CompilationUnit( + val inPaths: Seq[String], + val outPath: String, + val cmd: Seq[String], + ) { + val digestPath = s"$outPath.dig" + private val sortedInPaths = inPaths.sorted + + private def addIntToDigest(n: Int)(implicit digest: MessageDigest): Unit = + digest.update(String.format("%08x", n).getBytes("UTF-8")) + + private def addStringToDigest(s: String)(implicit + digest: MessageDigest, + ): Unit = + addBytesToDigest(s.getBytes("UTF-8")) + + private def addBytesToDigest( + b: Array[Byte], + )(implicit digest: MessageDigest): Unit = { + addIntToDigest(b.length) + digest.update(b) + } + + private def digestInsWithCmd(): String = { + implicit val digest = MessageDigest.getInstance("SHA-256") + addIntToDigest(inPaths.length) + for { inPath <- inPaths.sorted } { + addStringToDigest(inPath) + addBytesToDigest(Files.readAllBytes(Paths.get(inPath))) + } + addIntToDigest(cmd.length) + for { el <- cmd } + addStringToDigest(el) + HexFormat.of().formatHex(digest.digest()) + } + + def isUpToDate(): Boolean = + Files.exists(Paths.get(digestPath)) && Files.readString( + Paths.get(digestPath), + ) == digestInsWithCmd() + + def markUpToDate(): Unit = + writePath(digestPath, digestInsWithCmd()) + } } diff --git a/src/main/scala/ee/hrzn/chryse/tasks/BuildTask.scala b/src/main/scala/ee/hrzn/chryse/tasks/BuildTask.scala index 6034522..20a3abd 100644 --- a/src/main/scala/ee/hrzn/chryse/tasks/BuildTask.scala +++ b/src/main/scala/ee/hrzn/chryse/tasks/BuildTask.scala @@ -26,19 +26,20 @@ object BuildTask extends BaseTask { platform(genTop(platform)), firtoolOpts = firtoolOpts, ) - writeCare(verilogPath, verilog) + writePath(verilogPath, verilog) val yosysScriptPath = s"$buildDir/$name-${platform.id}.ys" val jsonPath = s"$buildDir/$name-${platform.id}.json" - writeCare( + writePath( yosysScriptPath, s"""read_verilog -sv $verilogPath |synth_ice40 -top top |write_json $jsonPath""".stripMargin, ) - runCare( - "synthesis", + val yosysCu = CompilationUnit( + Seq(verilogPath, yosysScriptPath), + jsonPath, Seq( "yosys", "-q", @@ -49,12 +50,15 @@ object BuildTask extends BaseTask { yosysScriptPath, ), ) + runCu("synthesis", yosysCu) // TODO: generate PCF. + val pcfPath = s"$name-${platform.id}.pcf" val ascPath = s"$buildDir/$name-${platform.id}.asc" - runCare( - "place and route", + val ascCu = CompilationUnit( + Seq(jsonPath, pcfPath), + ascPath, Seq( platform.nextpnrBinary, "-q", @@ -63,21 +67,24 @@ object BuildTask extends BaseTask { "--json", jsonPath, "--pcf", - s"$name-${platform.id}.pcf", + pcfPath, "--asc", ascPath, ) ++ platform.nextpnrArgs, ) + runCu("place and route", ascCu) val binPath = s"$buildDir/$name-${platform.id}.bin" - runCare( - "pack", + val binCu = CompilationUnit( + Seq(ascPath), + binPath, Seq(platform.packBinary, ascPath, binPath), ) + runCu("pack", binCu) if (program) { println(s"Programming ${platform.id} ...") - runCare("program", Seq(platform.programBinary, binPath)) + runCmd("program", Seq(platform.programBinary, binPath)) } } } diff --git a/src/main/scala/ee/hrzn/chryse/tasks/CxxsimTask.scala b/src/main/scala/ee/hrzn/chryse/tasks/CxxsimTask.scala index 8818325..c7b8e90 100644 --- a/src/main/scala/ee/hrzn/chryse/tasks/CxxsimTask.scala +++ b/src/main/scala/ee/hrzn/chryse/tasks/CxxsimTask.scala @@ -8,17 +8,19 @@ import ee.hrzn.chryse.platform.Platform import ee.hrzn.chryse.platform.cxxrtl.CXXRTLOptions import ee.hrzn.chryse.platform.cxxrtl.CXXRTLPlatform -import java.nio.file.FileVisitOption import java.nio.file.Files import java.nio.file.Paths import scala.jdk.CollectionConverters._ +import scala.sys.process._ object CxxsimTask extends BaseTask { private val cxxsimDir = "cxxsim" + private val cxxOpts = Seq("-std=c++14", "-g", "-pedantic", "-Wall", "-Wextra", + "-Wno-zero-length-array", "-Wno-unused-parameter") def apply( name: String, - genTop: Platform => HasIO[_ <: chisel3.Data], + genTop: Platform => HasIO[_ <: Data], cxxrtlOptions: CXXRTLOptions, config: ChryseAppConfig, ): Unit = { @@ -34,23 +36,24 @@ object CxxsimTask extends BaseTask { platform(genTop(platform)), firtoolOpts = firtoolOpts, ) - writeCare(verilogPath, verilog) + writePath(verilogPath, verilog) val blackboxIlPath = s"$buildDir/$name-${platform.id}-blackbox.il" // TODO - writeCare(blackboxIlPath, "\n") + writePath(blackboxIlPath, "\n") val yosysScriptPath = s"$buildDir/$name-${platform.id}.ys" val ccPath = s"$buildDir/$name.cc" - writeCare( + writePath( yosysScriptPath, s"""read_rtlil $blackboxIlPath |read_verilog -sv $verilogPath |write_cxxrtl -header $ccPath""".stripMargin, ) - runCare( - "synthesis", + val yosysCu = CompilationUnit( + Seq(blackboxIlPath, verilogPath, yosysScriptPath), + ccPath, Seq( "yosys", "-q", @@ -61,15 +64,68 @@ object CxxsimTask extends BaseTask { yosysScriptPath, ), ) - - // Compile all sources in cxxsimDir. - // Use Make-like heuristics to determine what's up to date. - // TODO: continue here. - for { p <- Files.walk(Paths.get(cxxsimDir)).iterator.asScala } - println(s"p: $p") + runCu("synthesis", yosysCu) // TODO: we need to decide how the simulation gets driven. How do we offer // enough control to the user? Do we assume they/let them do all the setup // themselves? etc. + // + // Fundamentally, the user may have many different ways of driving the + // process. We want to facilitate connecting blackboxes etc., but what else? + // Hrmmm. Let's start simple (just compiling everything, like rainhdx), and + // then see where we go. + val ccs = Seq(ccPath) ++ filesInDirWithExt(cxxsimDir, ".cc") + val headers = filesInDirWithExt(cxxsimDir, ".h").toSeq + + val yosysDatDir = Seq("yosys-config", "--datdir").!!.trim() + + def buildPathForCc(cc: String) = + cc.replace(s"$cxxsimDir/", s"$buildDir/") + .replace(".cc", ".o") + + def compileCmdForCc(cc: String, obj: String) = Seq( + "c++", + s"-I$buildDir", + s"-I$yosysDatDir/include/backends/cxxrtl/runtime", + "-c", + cc, + "-o", + obj, + ) ++ cxxOpts + + // XXX: depend on what look like headers for now. + val cus = for { + cc <- ccs + obj = buildPathForCc(cc) + cmd = compileCmdForCc(cc, obj) + } yield CompilationUnit(Seq(cc) ++ headers, obj, cmd) + + runCus("compilation", cus) + + val binPath = s"$buildDir/$name" + val linkCu = CompilationUnit( + cus.map(_.outPath), + binPath, + Seq("c++", "-o", binPath) ++ cxxOpts ++ cus.map(_.outPath), + ) + runCu("linking", linkCu) + + if (config.cxxrtlCompileOnly) return + + val binArgs = if (config.cxxrtlVcd) Seq("--vcd") else Seq() + val binCmd = Seq(binPath) ++ binArgs + + println(s"running: $binCmd") + val rc = binCmd.! + + println(s"$name exited with return code $rc") } + + private def filesInDirWithExt(dir: String, ext: String): Iterator[String] = + Files + .walk(Paths.get(dir), 1) + .iterator + .asScala + .map(_.toString) + .filter(_.endsWith(ext)) }