From 6b270bbdda2d79c666fb28843dca1d8fff204e2b Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 25 Jan 2024 12:28:29 +0000 Subject: [PATCH] Sort out the interface bincompat surface (#269) --- .../interface/src/main/scala/Binding.scala | 297 ++++++++++++ .../src/main/scala/BindingBuilder.scala | 127 ++++++ .../interface/src/main/scala/Interface.scala | 428 ------------------ modules/interface/src/main/scala/Utils.scala | 34 ++ 4 files changed, 458 insertions(+), 428 deletions(-) create mode 100644 modules/interface/src/main/scala/Binding.scala create mode 100644 modules/interface/src/main/scala/BindingBuilder.scala create mode 100644 modules/interface/src/main/scala/Utils.scala diff --git a/modules/interface/src/main/scala/Binding.scala b/modules/interface/src/main/scala/Binding.scala new file mode 100644 index 0000000..85bcace --- /dev/null +++ b/modules/interface/src/main/scala/Binding.scala @@ -0,0 +1,297 @@ +package bindgen.interface + +import java.io.* +import java.lang.ProcessBuilder.Redirect +import java.nio.file.* +import scala.sys.process.ProcessLogger +import scala.util.control.NonFatal + +import BindingLang.* +import Utils.* +import Binding.Defaults + +class Binding private ( + impl: Binding.BindingArgs +) { self => + + def headerFile: File = impl.headerFile + def packageName: String = impl.packageName + def linkName: Option[String] = impl.linkName + def cImports: List[String] = impl.cImports + def clangFlags: List[String] = impl.clangFlags + def exclusivePrefixes: List[String] = impl.exclusivePrefixes + def logLevel: LogLevel = impl.logLevel + def systemIncludes: Includes = impl.systemIncludes + def noConstructor: Set[String] = impl.noConstructor + def opaqueStructs: Set[String] = impl.opaqueStructs + def multiFile: Boolean = impl.multiFile + def noComments: Boolean = impl.noComments + def noLocation: Boolean = impl.noLocation + def exportMode: Boolean = impl.exportMode + def externalPaths: Map[String, String] = impl.externalPaths + def externalNames: Map[String, String] = impl.externalNames + def bindgenArguments: List[String] = impl.bindgenArguments + def excludeSystemPaths: List[Path] = impl.excludeSystemPaths + def scalaFile: String = impl.scalaFile + def cFile: String = impl.cFile + + def withLinkName(name: String) = copy(_.copy(linkName = Some(name))) + + def addCImport(header: String): Binding = + copy(b => b.copy(cImports = b.cImports :+ header)) + + def addCImports(headers: Seq[String]): Binding = + copy(b => b.copy(cImports = b.cImports ++ headers)) + + def withCImports(headers: Seq[String]): Binding = + copy(b => b.copy(cImports = headers.toList)) + + def addClangFlag(flag: String): Binding = + copy(b => b.copy(clangFlags = b.clangFlags :+ flag)) + + def addClangFlag(flags: Seq[String]): Binding = + copy(b => b.copy(clangFlags = b.clangFlags ++ flags)) + + def withClangFlags(flags: Seq[String]): Binding = + copy(b => b.copy(clangFlags = flags.toList)) + + def withLogLevel(level: LogLevel): Binding = copy(_.copy(logLevel = level)) + + def withSystemIncludes(includes: Includes): Binding = copy( + _.copy(systemIncludes = includes) + ) + def withMultiFile(b: Boolean): Binding = copy(_.copy(multiFile = b)) + + def withNoComments(b: Boolean): Binding = copy(_.copy(noComments = b)) + + def withNoLocation(b: Boolean): Binding = copy(_.copy(noLocation = b)) + + def withExport(b: Boolean): Binding = copy(_.copy(exportMode = b)) + + def withPackageName(name: String) = copy(_.copy(packageName = name)) + + def withHeaderFile(header: File) = copy(_.copy(headerFile = header)) + + def withNoConstructor(structs: Set[String]): Binding = copy( + _.copy(noConstructor = structs) + ) + + def withOpaqueStructs(structs: Set[String]): Binding = copy( + _.copy(opaqueStructs = structs) + ) + + def withExternalPaths(externals: Map[String, String]): Binding = copy( + _.copy(externalPaths = externals) + ) + + def addExternalPath(fileFilter: String, packageName: String): Binding = + copy(b => + b.copy(externalPaths = b.externalPaths.updated(fileFilter, packageName)) + ) + + def addExternalPaths(externals: Map[String, String]): Binding = + copy(b => b.copy(externalPaths = b.externalPaths ++ externals)) + + def withExternalNames(externals: Map[String, String]): Binding = copy( + _.copy(externalNames = externals) + ) + + def addExternalName(nameFilter: String, packageName: String): Binding = + copy(b => + b.copy(externalNames = b.externalNames.updated(nameFilter, packageName)) + ) + + def addExternalNames(externals: Map[String, String]): Binding = + copy(b => b.copy(externalNames = b.externalNames ++ externals)) + + def addExcludedSystemPath(path: Path): Binding = + copy(b => b.copy(excludeSystemPaths = b.excludeSystemPaths :+ path)) + + def withExcludedSystemPaths(paths: List[Path]): Binding = + copy(b => b.copy(excludeSystemPaths = paths)) + + def withBindgenArguments(arguments: List[String]): Binding = + copy(_.copy(bindgenArguments = arguments)) + + def addBindgenArgument(argument: String): Binding = + copy(b => b.copy(bindgenArguments = b.bindgenArguments :+ argument)) + + def addBindgenArguments(arguments: List[String]): Binding = + copy(b => b.copy(bindgenArguments = b.bindgenArguments ++ arguments)) + + def toCommand(lang: BindingLang): List[String] = { + val sb = List.newBuilder[String] + + def arg(name: String, value: String) = + sb ++= Seq(s"--$name", value) + + def flag(name: String) = + sb += s"--$name" + + arg( + "header", + headerFile.toPath().toAbsolutePath().toString + ) + + arg("package", packageName) + + linkName.foreach { link => + arg("link-name", link) + } + cImports.foreach { cimp => + arg("c-import", cimp) + } + clangFlags.foreach { clangFlag => + arg("clang", clangFlag) + } + exclusivePrefixes.foreach { prefix => + arg("exclusive-prefix", prefix) + } + + if (noConstructor.nonEmpty) + arg("render.no-constructor", noConstructor.toList.sorted.mkString(",")) + + if (opaqueStructs.nonEmpty) + arg("render.opaque-structs", opaqueStructs.toList.sorted.mkString(",")) + + flag(logLevel.str) + if (lang == BindingLang.Scala) + flag("scala") + else + flag("c") + + if (multiFile && lang == BindingLang.Scala) flag("multi-file") + if (noComments && lang == BindingLang.Scala) flag("render.no-comments") + if (noLocation && lang == BindingLang.Scala) flag("render.no-location") + if (exportMode && lang == BindingLang.Scala) flag("export") + + externalPaths.toList.sorted.map { case (filter, pkg) => + arg("render.external-path", s"$filter=$pkg") + } + + externalNames.toList.sorted.map { case (filter, pkg) => + arg("render.external-name", s"$filter=$pkg") + } + + excludeSystemPaths.map { case path => + arg("exclude-system-path", path.toString()) + } + + sb ++= bindgenArguments + + sb.result() + } + + private def copy(f: Binding.BindingArgs => Binding.BindingArgs) = + new Binding(f(impl)) + + @deprecated( + "The binding builder is deprecated. This method is only preserved for ease of migration. " + + "You can safely remove the call to this method. " + + "This method will be removed in 0.1.0", + "0.0.24" + ) + def build: Binding = this + +} + +object Binding { + def apply(header: File, packageName: String): Binding = + new Binding( + BindingArgs( + headerFile = header, + packageName = packageName, + cFile = s"$packageName.c", + scalaFile = s"$packageName.scala" + ) + ) + + @deprecated( + "The binding builder is deprecated. This method is only preserved for ease of migration. " + + "Use `apply(...)` and call builder methods on binding directly. " + + "This method will be removed in 0.1.0", + "0.0.24" + ) + def builder(header: File, packageName: String): Binding = + apply(header, packageName) + + @deprecated( + "This method will be removed, please use the builder pattern instead" + + " - Binding(headerFile, packageName).withParam(...).withOtherParam(...)...", + "0.0.23" + ) + def apply( + headerFile: File, + packageName: String, + linkName: Option[String] = Defaults.linkName, + cImports: List[String] = Defaults.cImports, + clangFlags: List[String] = Defaults.clangFlags, + exclusivePrefixes: List[String] = Nil, + logLevel: LogLevel = Defaults.logLevel, + systemIncludes: Includes = Defaults.systemIncludes, + noConstructor: Set[String] = Defaults.noConstructor, + opaqueStructs: Set[String] = Defaults.opaqueStructs, + multiFile: Boolean = Defaults.multiFile, + noComments: Boolean = Defaults.noComments, + noLocation: Boolean = Defaults.noLocation + ): Binding = { + apply(headerFile, packageName).copy( + _.copy( + linkName = linkName, + cImports = cImports, + clangFlags = clangFlags, + exclusivePrefixes = exclusivePrefixes, + logLevel = logLevel, + systemIncludes = systemIncludes, + noConstructor = noConstructor, + opaqueStructs = opaqueStructs, + multiFile = multiFile, + noComments = noComments, + noLocation = noLocation + ) + ) + } + + private[interface] case class BindingArgs( + headerFile: File, + packageName: String, + linkName: Option[String] = Defaults.linkName, + cImports: List[String] = Defaults.cImports, + clangFlags: List[String] = Defaults.clangFlags, + exclusivePrefixes: List[String] = Defaults.exclusivePrefixes, + logLevel: LogLevel = Defaults.logLevel, + systemIncludes: Includes = Defaults.systemIncludes, + noConstructor: Set[String] = Defaults.noConstructor, + opaqueStructs: Set[String] = Defaults.opaqueStructs, + multiFile: Boolean = Defaults.multiFile, + noComments: Boolean = Defaults.noComments, + noLocation: Boolean = Defaults.noLocation, + exportMode: Boolean = Defaults.exportMode, + externalPaths: Map[String, String] = Defaults.externalPaths, + externalNames: Map[String, String] = Defaults.externalNames, + bindgenArguments: List[String] = Defaults.bindgenArguments, + excludeSystemPaths: List[Path] = Defaults.excludeSystemPaths, + scalaFile: String, + cFile: String + ) + + private[interface] object Defaults { + val linkName = None + val cImports = List.empty[String] + val clangFlags = List.empty[String] + val logLevel = LogLevel.Warn + val systemIncludes = Includes.ClangSearchPath + val noConstructor = Set.empty[String] + val opaqueStructs = Set.empty[String] + val exclusivePrefixes = List.empty[String] + val multiFile = false + val noComments = false + val noLocation = false + val externalPaths = Map.empty[String, String] + val externalNames = Map.empty[String, String] + val bindgenArguments = List.empty[String] + val excludeSystemPaths = List.empty[Path] + val exportMode = false + } + +} diff --git a/modules/interface/src/main/scala/BindingBuilder.scala b/modules/interface/src/main/scala/BindingBuilder.scala new file mode 100644 index 0000000..dcfbfca --- /dev/null +++ b/modules/interface/src/main/scala/BindingBuilder.scala @@ -0,0 +1,127 @@ +package bindgen.interface + +import java.io.* +import java.lang.ProcessBuilder.Redirect +import java.nio.file.* +import scala.sys.process.ProcessLogger +import scala.util.control.NonFatal +import BindingLang.* +import Utils.* +import Binding.Defaults + +class BindingBuilder( + binary: File, + errPrintln: String => Unit = s => System.err.println(s) +) { + assert( + Files.exists(binary.toPath), + s"Bindgen: specified binary [$binary] doesn't exist!" + ) + assert( + Files.isRegularFile(binary.toPath), + s"Bindgen: specified binary [$binary] is not a regular file!" + ) + + def generate( + bindings: Seq[Binding], + destinationDir: File, + lang: BindingLang, + clangPath: Option[Path] + ): Seq[File] = { + + val files = Seq.newBuilder[File] + + bindings.foreach { binding => + import scala.sys.process.Process + + val destinationFilename = lang match { + case Scala => binding.scalaFile + case C => binding.cFile + } + + val destination = if (!binding.multiFile || lang == BindingLang.C) { + val destinationFilename = lang match { + case Scala => binding.scalaFile + case C => binding.cFile + } + + destinationDir / destinationFilename + } else { + val dir = destinationDir / binding.packageName + + Files.createDirectories(dir.toPath) + + dir + } + + val platformArgs = + if (binding.systemIncludes == Includes.None) { + List("--no-system") + } else { + clangPath match { + case None => Nil + case Some(value) => List("--clang-path", value.toString) + } + } + + val outputArgs = + Seq("--out", destination.toPath().toString(), "--print-files") + + val cmd = + binary.toString :: binding.toCommand(lang) ++ platformArgs ++ outputArgs + + val escapeArgs = cmd + .map { arg => + if (arg contains ' ') s"'$arg'" + else arg + } + .mkString(" ") + + val stderr = List.newBuilder[String] + val stdout = List.newBuilder[String] + + val logger = ProcessLogger.apply( + (o: String) => stdout += o, + (e: String) => stderr += e + ) + + val process = new java.lang.ProcessBuilder(cmd*) + .start() + + io.Source + .fromInputStream(process.getErrorStream()) + .getLines() + .foreach(errPrintln(_)) + + io.Source + .fromInputStream(process.getInputStream()) + .getLines() + .foreach(logger.out(_)) + + val result = process.waitFor() + + files ++= stdout + .result() + .map { path => new File(path) } + .filter(_.isFile()) + + if (result == 0) { + errPrintln( + s"Successfully regenerated binding ($lang) for ${binding.packageName}, $result" + ) + } else { + val code = destination.hashCode().toHexString.toUpperCase() + errPrintln( + s"(FAILED [$code]) Executing [$escapeArgs]" + ) + + stderr.result().foreach(errPrintln) + + throw new Exception( + s"Process [$escapeArgs] failed with code $result" + ) + } + } + files.result() + } +} diff --git a/modules/interface/src/main/scala/Interface.scala b/modules/interface/src/main/scala/Interface.scala index 32356be..7b7f426 100644 --- a/modules/interface/src/main/scala/Interface.scala +++ b/modules/interface/src/main/scala/Interface.scala @@ -36,436 +36,8 @@ object LogLevel { } import BindingLang.* -object Utils { - private[interface] implicit class FileOps(val f: File) extends AnyVal { - def /(other: String): File = { - val result = Paths.get(f.toPath.toString, other).toFile - Files.createDirectories(f.toPath()) - result - } - } - private[interface] def fileWriter(destination: File)(f: Writer => Unit) = { - var fw: Option[BufferedWriter] = None - try { - fw = Option( - new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(destination)) - ) - ) - - fw.foreach(f) - } catch { - case NonFatal(ex) => fw.foreach(_.close()); throw ex - } finally { - fw.foreach(_.close()) - } - } -} - -import Utils.* - -import Binding.Defaults - -class Binding private ( - val headerFile: File, - val packageName: String, - val linkName: Option[String] = Defaults.linkName, - val cImports: List[String] = Defaults.cImports, - val clangFlags: List[String] = Defaults.clangFlags, - val exclusivePrefixes: List[String] = Defaults.exclusivePrefixes, - val logLevel: LogLevel = Defaults.logLevel, - val systemIncludes: Includes = Defaults.systemIncludes, - val noConstructor: Set[String] = Defaults.noConstructor, - val opaqueStructs: Set[String] = Defaults.opaqueStructs, - val multiFile: Boolean = Defaults.multiFile, - val noComments: Boolean = Defaults.noComments, - val noLocation: Boolean = Defaults.noLocation, - val exportMode: Boolean = Defaults.exportMode, - val externalPaths: Map[String, String] = Defaults.externalPaths, - val externalNames: Map[String, String] = Defaults.externalNames, - val bindgenArguments: List[String] = Defaults.bindgenArguments, - val excludeSystemPaths: List[Path] = Defaults.excludeSystemPaths, - val scalaFile: String, - val cFile: String -) { self => - - private def copy( - headerFile: File = self.headerFile, - packageName: String = self.packageName, - linkName: Option[String] = self.linkName, - cImports: List[String] = self.cImports, - clangFlags: List[String] = self.clangFlags, - exclusivePrefixes: List[String] = self.exclusivePrefixes, - logLevel: LogLevel = self.logLevel, - systemIncludes: Includes = self.systemIncludes, - noConstructor: Set[String] = self.noConstructor, - opaqueStructs: Set[String] = self.opaqueStructs, - multiFile: Boolean = self.multiFile, - noComments: Boolean = self.noComments, - noLocation: Boolean = self.noLocation, - exportMode: Boolean = self.exportMode, - externalPaths: Map[String, String] = self.externalPaths, - externalNames: Map[String, String] = self.externalNames, - bindgenArguments: List[String] = self.bindgenArguments, - excludeSystemPaths: List[Path] = self.excludeSystemPaths, - scalaFile: String = self.scalaFile, - cFile: String = self.cFile - ) = - new Binding( - headerFile = headerFile, - packageName = packageName, - linkName = linkName, - cImports = cImports, - clangFlags = clangFlags, - exclusivePrefixes = exclusivePrefixes, - logLevel = logLevel, - systemIncludes = systemIncludes, - noConstructor = noConstructor, - opaqueStructs = opaqueStructs, - multiFile = multiFile, - noComments = noComments, - noLocation = noLocation, - exportMode = exportMode, - externalPaths = externalPaths, - externalNames = externalNames, - bindgenArguments = bindgenArguments, - excludeSystemPaths = excludeSystemPaths, - scalaFile = scalaFile, - cFile = cFile - ) - - def toCommand(lang: BindingLang): List[String] = { - val sb = List.newBuilder[String] - - def arg(name: String, value: String) = - sb ++= Seq(s"--$name", value) - - def flag(name: String) = - sb += s"--$name" - - arg( - "header", - headerFile.toPath().toAbsolutePath().toString - ) - - arg("package", packageName) - - linkName.foreach { link => - arg("link-name", link) - } - cImports.foreach { cimp => - arg("c-import", cimp) - } - clangFlags.foreach { clangFlag => - arg("clang", clangFlag) - } - exclusivePrefixes.foreach { prefix => - arg("exclusive-prefix", prefix) - } - - if (noConstructor.nonEmpty) - arg("render.no-constructor", noConstructor.toList.sorted.mkString(",")) - - if (opaqueStructs.nonEmpty) - arg("render.opaque-structs", opaqueStructs.toList.sorted.mkString(",")) - - flag(logLevel.str) - if (lang == BindingLang.Scala) - flag("scala") - else - flag("c") - - if (multiFile && lang == BindingLang.Scala) flag("multi-file") - if (noComments && lang == BindingLang.Scala) flag("render.no-comments") - if (noLocation && lang == BindingLang.Scala) flag("render.no-location") - if (exportMode && lang == BindingLang.Scala) flag("export") - - externalPaths.toList.sorted.map { case (filter, pkg) => - arg("render.external-path", s"$filter=$pkg") - } - - externalNames.toList.sorted.map { case (filter, pkg) => - arg("render.external-name", s"$filter=$pkg") - } - - excludeSystemPaths.map { case path => - arg("exclude-system-path", path.toString()) - } - - sb ++= bindgenArguments - - sb.result() - } - -} - -object Binding { - def builder(header: File, packageName: String) = - new Builder( - new Binding( - headerFile = header, - packageName = packageName, - cFile = s"$packageName.c", - scalaFile = s"$packageName.scala" - ) - ) - - class Builder private[Binding] (binding: Binding) { - private def copy(f: Binding => Binding): Builder = - new Builder(f(binding)) - - def withLinkName(name: String) = copy(_.copy(linkName = Some(name))) - - def addCImport(header: String): Builder = - copy(b => b.copy(cImports = b.cImports :+ header)) - def addCImports(headers: Seq[String]): Builder = - copy(b => b.copy(cImports = b.cImports ++ headers)) - def withCImports(headers: Seq[String]): Builder = - copy(b => b.copy(cImports = headers.toList)) - - def addClangFlag(flag: String): Builder = - copy(b => b.copy(clangFlags = b.clangFlags :+ flag)) - def addClangFlag(flags: Seq[String]): Builder = - copy(b => b.copy(clangFlags = b.clangFlags ++ flags)) - def withClangFlags(flags: Seq[String]): Builder = - copy(b => b.copy(clangFlags = flags.toList)) - - def withLogLevel(level: LogLevel): Builder = copy(_.copy(logLevel = level)) - def withSystemIncludes(includes: Includes): Builder = copy( - _.copy(systemIncludes = includes) - ) - def withMultiFile(b: Boolean): Builder = copy(_.copy(multiFile = b)) - def withNoComments(b: Boolean): Builder = copy(_.copy(noComments = b)) - def withNoLocation(b: Boolean): Builder = copy(_.copy(noLocation = b)) - def withExport(b: Boolean): Builder = copy(_.copy(exportMode = b)) - - def withPackageName(name: String) = copy(_.copy(packageName = name)) - def withHeaderFile(header: File) = copy(_.copy(headerFile = header)) - - def withNoConstructor(structs: Set[String]) = copy( - _.copy(noConstructor = structs) - ) - def withOpaqueStructs(structs: Set[String]) = copy( - _.copy(opaqueStructs = structs) - ) - - def withExternalPaths(externals: Map[String, String]) = copy( - _.copy(externalPaths = externals) - ) - def addExternalPath(fileFilter: String, packageName: String) = copy(b => - b.copy(externalPaths = b.externalPaths.updated(fileFilter, packageName)) - ) - def addExternalPaths(externals: Map[String, String]) = - copy(b => b.copy(externalPaths = b.externalPaths ++ externals)) - - def withExternalNames(externals: Map[String, String]) = copy( - _.copy(externalNames = externals) - ) - def addExternalName(nameFilter: String, packageName: String) = copy(b => - b.copy(externalNames = b.externalNames.updated(nameFilter, packageName)) - ) - def addExternalNames(externals: Map[String, String]) = - copy(b => b.copy(externalNames = b.externalNames ++ externals)) - - def addExcludedSystemPath(path: Path) = - copy(b => b.copy(excludeSystemPaths = b.excludeSystemPaths :+ path)) - - def withExcludedSystemPaths(paths: List[Path]) = - copy(b => b.copy(excludeSystemPaths = paths)) - - def withBindgenArguments(arguments: List[String]) = - copy(_.copy(bindgenArguments = arguments)) - def addBindgenArgument(argument: String) = - copy(b => b.copy(bindgenArguments = b.bindgenArguments :+ argument)) - def addBindgenArguments(arguments: List[String]) = - copy(b => b.copy(bindgenArguments = b.bindgenArguments ++ arguments)) - - def build: Binding = binding - } - - object Defaults { - val linkName = None - val cImports = List.empty[String] - val clangFlags = List.empty[String] - val logLevel = LogLevel.Warn - val systemIncludes = Includes.ClangSearchPath - val noConstructor = Set.empty[String] - val opaqueStructs = Set.empty[String] - val exclusivePrefixes = List.empty[String] - val multiFile = false - val noComments = false - val noLocation = false - val externalPaths = Map.empty[String, String] - val externalNames = Map.empty[String, String] - val bindgenArguments = List.empty[String] - val excludeSystemPaths = List.empty[Path] - val exportMode = false - } - - def apply(headerFile: File, packageName: String): Binding = - builder(headerFile, packageName).build - - @deprecated( - "This method will be removed, please use the builder pattern instead" + - " - Binding.builder(headerFile, packageName).withParam.build...", - "0.0.16" - ) - def apply( - headerFile: File, - packageName: String, - linkName: Option[String] = Defaults.linkName, - cImports: List[String] = Defaults.cImports, - clangFlags: List[String] = Defaults.clangFlags, - exclusivePrefixes: List[String] = Nil, - logLevel: LogLevel = Defaults.logLevel, - systemIncludes: Includes = Defaults.systemIncludes, - noConstructor: Set[String] = Defaults.noConstructor, - opaqueStructs: Set[String] = Defaults.opaqueStructs, - multiFile: Boolean = Defaults.multiFile, - noComments: Boolean = Defaults.noComments, - noLocation: Boolean = Defaults.noLocation - ): Binding = { - new Binding( - headerFile = headerFile, - packageName = packageName, - linkName = linkName, - cFile = s"$packageName.c", - scalaFile = s"$packageName.scala", - cImports = cImports, - clangFlags = clangFlags, - exclusivePrefixes = exclusivePrefixes, - logLevel = logLevel, - systemIncludes = systemIncludes, - noConstructor = noConstructor, - opaqueStructs = opaqueStructs, - multiFile = multiFile, - noComments = noComments, - noLocation = noLocation, - externalPaths = Defaults.externalPaths, - externalNames = Defaults.externalNames - ) - } -} - sealed trait Includes extends Product with Serializable object Includes { case object ClangSearchPath extends Includes case object None extends Includes } - -class BindingBuilder( - binary: File, - errPrintln: String => Unit = s => System.err.println(s) -) { - assert( - Files.exists(binary.toPath), - s"Bindgen: specified binary [$binary] doesn't exist!" - ) - assert( - Files.isRegularFile(binary.toPath), - s"Bindgen: specified binary [$binary] is not a regular file!" - ) - - def generate( - bindings: Seq[Binding], - destinationDir: File, - lang: BindingLang, - clangPath: Option[Path] - ): Seq[File] = { - - val files = Seq.newBuilder[File] - - bindings.foreach { binding => - import scala.sys.process.Process - - val destinationFilename = lang match { - case Scala => binding.scalaFile - case C => binding.cFile - } - - val destination = if (!binding.multiFile || lang == BindingLang.C) { - val destinationFilename = lang match { - case Scala => binding.scalaFile - case C => binding.cFile - } - - destinationDir / destinationFilename - } else { - val dir = destinationDir / binding.packageName - - Files.createDirectories(dir.toPath) - - dir - } - - val platformArgs = - if (binding.systemIncludes == Includes.None) { - List("--no-system") - } else { - clangPath match { - case None => Nil - case Some(value) => List("--clang-path", value.toString) - } - } - - val outputArgs = - Seq("--out", destination.toPath().toString(), "--print-files") - - val cmd = - binary.toString :: binding.toCommand(lang) ++ platformArgs ++ outputArgs - - val escapeArgs = cmd - .map { arg => - if (arg contains ' ') s"'$arg'" - else arg - } - .mkString(" ") - - val stderr = List.newBuilder[String] - val stdout = List.newBuilder[String] - - val logger = ProcessLogger.apply( - (o: String) => stdout += o, - (e: String) => stderr += e - ) - - val process = new java.lang.ProcessBuilder(cmd*) - .start() - - io.Source - .fromInputStream(process.getErrorStream()) - .getLines() - .foreach(errPrintln(_)) - - io.Source - .fromInputStream(process.getInputStream()) - .getLines() - .foreach(logger.out(_)) - - val result = process.waitFor() - - files ++= stdout - .result() - .map { path => new File(path) } - .filter(_.isFile()) - - if (result == 0) { - errPrintln( - s"Successfully regenerated binding ($lang) for ${binding.packageName}, $result" - ) - } else { - val code = destination.hashCode().toHexString.toUpperCase() - errPrintln( - s"(FAILED [$code]) Executing [$escapeArgs]" - ) - - stderr.result().foreach(errPrintln) - - throw new Exception( - s"Process [$escapeArgs] failed with code $result" - ) - } - } - files.result() - } -} diff --git a/modules/interface/src/main/scala/Utils.scala b/modules/interface/src/main/scala/Utils.scala new file mode 100644 index 0000000..cca49c4 --- /dev/null +++ b/modules/interface/src/main/scala/Utils.scala @@ -0,0 +1,34 @@ +package bindgen.interface + +import java.io.* +import java.lang.ProcessBuilder.Redirect +import java.nio.file.* +import scala.sys.process.ProcessLogger +import scala.util.control.NonFatal +import BindingLang.* + +private[interface] object Utils { + private[interface] implicit class FileOps(val f: File) extends AnyVal { + def /(other: String): File = { + val result = Paths.get(f.toPath.toString, other).toFile + Files.createDirectories(f.toPath()) + result + } + } + private[interface] def fileWriter(destination: File)(f: Writer => Unit) = { + var fw: Option[BufferedWriter] = None + try { + fw = Option( + new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(destination)) + ) + ) + + fw.foreach(f) + } catch { + case NonFatal(ex) => fw.foreach(_.close()); throw ex + } finally { + fw.foreach(_.close()) + } + } +}