All Downloads are FREE. Search and download functionalities are using the official Maven repository.

izumi.idealingua.compiler.CommandlineIDLCompiler.scala Maven / Gradle / Ivy

There is a newer version: 1.3.16
Show newest version
package izumi.idealingua.compiler

import com.typesafe.config.ConfigFactory
import io.circe
import io.circe.parser.parse
import io.circe.syntax.*
import io.circe.{Json, JsonObject}
import izumi.fundamentals.platform.files.IzFiles
import izumi.fundamentals.platform.language.Quirks.*
import izumi.fundamentals.platform.resources.{IzArtifactMaterializer, IzResourcesDirty}
import izumi.fundamentals.platform.strings.IzString.*
import izumi.fundamentals.platform.time.Timed
import izumi.idealingua.compiler.Codecs.*
import izumi.idealingua.il.loader.{LocalModelLoaderContext, ModelResolver}
import izumi.idealingua.model.loader.UnresolvedDomains
import izumi.idealingua.model.publishing.{BuildManifest, ProjectVersion}
import izumi.idealingua.translator.*

import java.nio.file.*
import java.time.{ZoneId, ZonedDateTime}
import scala.jdk.CollectionConverters.*
import scala.util.Try

object CommandlineIDLCompiler {
  private val log: CompilerLog   = CompilerLog.Default
  private val shutdown: Shutdown = ShutdownImpl

  def main(args: Array[String]): Unit = {
    val mf               = IzArtifactMaterializer.currentArtifact
    val izumiVersion     = mf.version.toString
    val izumiInfoVersion = mf.justVersion

    log.log(s"Izumi IDL Compiler $izumiInfoVersion")

    val conf = parseArgs(args)

    val results = Seq(
      initDir(conf),
      runCompilations(izumiVersion, conf),
      runPublish(conf),
    )

    if (!results.contains(true)) {
      log.log("There was nothing to do. Try to run with `:help`")
    }
  }

  private def runPublish(conf: IDLCArgs): Boolean = {
    if (conf.publish && conf.languages.nonEmpty) {
      conf.languages.foreach {
        lang =>
          val manifest = toOptions(conf, Map.empty)(lang).manifest
          publishLangArtifacts(conf, lang, manifest) match {
            case Left(err) => throw err
            case _         => ()
          }
      }
      true
    } else
      false
  }

  def publishLangArtifacts(conf: IDLCArgs, langOpts: LanguageOpts, manifest: BuildManifest): Either[Throwable, Unit] = for {
    credsFile <- Either.cond(
      langOpts.credentials.isDefined,
      langOpts.credentials.get,
      new IllegalArgumentException(
        s"Can't publish ${langOpts.id} with empty credentials file. " +
        s"Use `--credentials` command line arg to set it"
      ),
    )
    lang   <- Try(IDLLanguage.parse(langOpts.id)).toEither
    creds  <- new CredentialsReader(lang, credsFile).read(toJson(langOpts.overrides))
    target <- Try(conf.target.toAbsolutePath.resolve(langOpts.id)).toEither
    res    <- new ArtifactPublisher(target, lang, creds, manifest).publish()
  } yield res

  private def initDir(conf: IDLCArgs): Boolean = {
    conf.init match {
      case Some(p) =>
        log.log(s"Initializing layout in `$p` ...")
        val f = p.toFile
        if (f.exists()) {
          if (f.isDirectory) {
            if (f.listFiles().nonEmpty) {
              shutdown.shutdown(s"Exists and not empty: $p")
            }
          } else {
            shutdown.shutdown(s"Exists and not a directory: $p")
          }
        }

        val mfdir = p.resolve("manifests")
        mfdir.toFile.mkdirs().discard()
        IzResourcesDirty.copyFromClasspath("defs/example", p).discard()

        TypespaceCompilerBaseFacade.descriptors.foreach {
          d =>
            Files.write(mfdir.resolve(s"${d.language.toString.toLowerCase}.json"), new ManifestWriter().write(d.defaultManifest).utf8).discard()
        }

        Files.write(p.resolve(s"version.json"), VersionOverlay.example.asJson.toString().utf8).discard()
        true
      case None =>
        false
    }
  }

  private def runCompilations(izumiVersion: String, conf: IDLCArgs): Boolean = {
    if (conf.languages.nonEmpty) {
      log.log("Reading manifests...")
      val toRun = conf.languages.map(toOptions(conf, Map("common.izumiVersion" -> izumiVersion)))
      log.log("Going to compile:")
      log.log(toRun.niceList())
      log.log("")

      val path   = conf.source.toAbsolutePath
      val target = conf.target.toAbsolutePath
      target.toFile.mkdirs()

      log.log(s"Loading definitions from `$path`...")

      val loaded = Timed {
        if (path.toFile.exists() && path.toFile.isDirectory) {
          val context = new LocalModelLoaderContext(Seq(path, conf.overlay.toAbsolutePath), Seq.empty)
          context.loader.load()
        } else {
          shutdown.shutdown(s"Not exists or not a directory: $path")
        }
      }
      log.log(s"Done: ${loaded.value.domains.results.size} in ${loaded.duration.toMillis}ms")
      log.log("")

      toRun.foreach {
        option =>
          runCompiler(target, loaded, option)
      }
      true
    } else {
      false
    }
  }

  private def runCompiler(target: Path, loaded: Timed[UnresolvedDomains], option: UntypedCompilerOptions): Unit = {
    val langId  = option.language.toString
    val itarget = option.target.getOrElse(target.resolve(langId))
    log.log(s"Preparing typespace for $langId")
    val toCompile = Timed {
      val rules = TypespaceCompilerBaseFacade.descriptor(option.language).rules
      new ModelResolver(rules)
        .resolve(loaded.value)
        .ifWarnings {
          message =>
            log.log(message)
        }
        .ifFailed {
          message =>
            shutdown.shutdown(message)
        }
        .successful
    }
    log.log(s"Finished in ${toCompile.duration.toMillis}ms")

    val out = Timed {
      new TypespaceCompilerFSFacade(toCompile)
        .compile(itarget, option)
    }

    val allPaths = out.compilationProducts.paths

    log.log(s"${allPaths.size} source files from ${toCompile.size} domains produced in `$itarget` in ${out.duration.toMillis}ms")
    log.log(s"Archive: ${out.zippedOutput}")
    log.log("")
  }

  private def parseArgs(args: Array[String]): IDLCArgs = {
    IDLCArgs.parseUnsafe(args)
  }

  private def toOptions(conf: IDLCArgs, env: Map[String, String])(lopt: LanguageOpts): UntypedCompilerOptions = {
    val lang = IDLLanguage.parse(lopt.id)
    val exts = getExt(lang, lopt.extensions)

    val manifest = readManifest(conf, env, lopt, lang)
    UntypedCompilerOptions(lang, exts, lopt.target, manifest, lopt.withRuntime, zipOutput = lopt.zip)
  }

  private def readManifest(conf: IDLCArgs, env: Map[String, String], lopt: LanguageOpts, lang: IDLLanguage): BuildManifest = {
    val default = conf.root.resolve("version.json")

    val overlay = conf.versionOverlay.map(loadVersionOverlay(lang)) match {
      case Some(value) =>
        Some(value)
      case None if default.toFile.exists() =>
        log.log(s"Found $default, using as version overlay for $lang...")
        Some(loadVersionOverlay(lang)(default))
      case None =>
        None
    }

    val overlayJson = overlay match {
      case Some(Right(value)) =>
        value
      case Some(Left(e)) =>
        shutdown.shutdown(s"Failed to parse version overlay: ${e.getMessage}")
      case None =>
        JsonObject.empty.asJson
    }

    val envJson               = toJson(env)
    val languageOverridesJson = toJson(lopt.overrides)
    val globalOverridesJson   = toJson(conf.overrides)
    val patch                 = overlayJson.deepMerge(globalOverridesJson).deepMerge(envJson).deepMerge(languageOverridesJson)

    val reader   = new ManifestReader(conf, log, shutdown, patch, lang, lopt.manifest)
    val manifest = reader.read()
    manifest
  }

  private def loadVersionOverlay(lang: IDLLanguage)(path: Path): Either[circe.Error, Json] = {
    import io.circe.literal.*

    for {
      parsed  <- parse(IzFiles.readString(path.toFile))
      decoded <- parsed.as[VersionOverlay]
    } yield {
      val defQualifier = decoded.snapshotQualifiers.getOrElse(lang.toString.toLowerCase, "UNSET")
      val timestamp    = ZonedDateTime.now(ZoneId.of("UTC")).toEpochSecond
      val qualifier    = if (lang == IDLLanguage.Typescript) s"$defQualifier-$timestamp" else defQualifier
      val version      = ProjectVersion(decoded.version, decoded.release, qualifier)
      json"""{"common": {"version": $version}}"""
    }
  }

  private def toJson(env: Map[String, String]) = {
    val updatedEnv = env.map {
      case (k, v) =>
        io.circe.parser.parse(v) match {
          case Right(b) if b.isBoolean => k -> b.asBoolean.get
          case Right(s) if s.isString  => k -> s.asString.get
          case _                       => k -> v
        }
    }
    valToJson(ConfigFactory.parseMap(updatedEnv.asJava).root().unwrapped())
  }

  private def valToJson(v: AnyRef): Json = {
    import io.circe.syntax.*

    (v: @unchecked) match {
      case m: java.util.HashMap[?, ?] =>
        m.asScala.map {
          case (k, value) =>
            k.toString -> valToJson(value.asInstanceOf[AnyRef])
        }.asJson

      case s: String =>
        s.asJson

      case b: java.lang.Boolean =>
        b.asJson
    }
  }

  private def getExt(lang: IDLLanguage, filter: List[String]): Seq[TranslatorExtension] = {
    val descriptor = TypespaceCompilerBaseFacade.descriptor(lang)
    val negative   = filter.filter(_.startsWith("-")).map(_.substring(1)).map(ExtensionId.apply).toSet
    descriptor.defaultExtensions.filterNot(e => negative.contains(e.id))
  }
}

case class VersionOverlay(version: String, release: Boolean, snapshotQualifiers: Map[String, String])

object VersionOverlay {
  def example: VersionOverlay = {
    VersionOverlay(
      "0.0.1",
      release = false,
      Map(
        IDLLanguage.Scala      -> "SNAPSHOT",
        IDLLanguage.Typescript -> "build.0",
        IDLLanguage.Go         -> "0",
        IDLLanguage.CSharp     -> "alpha",
      ).map { case (k, v) => k.toString.toLowerCase -> v },
    )
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy