假设我们想require
在 Scala 中构建一个类似 的功能,这在许多脚本语言中都是众所周知的。
如果我们需要这样的库...
require("some-library.jar")
...当执行 require 宏时,我们需要一种方法来将此 jar 添加到编译器的类路径中。如何才能做到这一点?
假设我们想require
在 Scala 中构建一个类似 的功能,这在许多脚本语言中都是众所周知的。
如果我们需要这样的库...
require("some-library.jar")
...当执行 require 宏时,我们需要一种方法来将此 jar 添加到编译器的类路径中。如何才能做到这一点?
另一个有趣的@kim-stebel 问题。
我的第一个想法(没有解决您的问题)是您的编译器可以使用 findMacroClassLoader 自定义宏类路径。由于@extempore,REPL 使用了它。
这对于像 requires("myfoo.jar") { mymacro } 这样的习语可能很有用。
您的问题是您是否可以更新编译器的类路径。这可以通过从您的上下文宇宙向下转换到编译器来实现,为此 isCompilerUniverse == true。然后你可以platform.updateClassPath。
用代码更新:
这类似于在所需宏的类路径中使用特殊占位符的想法。
下面提到的奇特方法是使用自定义类路径,该路径可以报告该位置的所有必需类。下面提到的旧代码用于类路径中的虚拟目录。
这种快速而俗气的方式使用文件系统来解压缩你的 jar,并要求编译器重新扫描它。
由于 invalidateClassPathEntries 的限制,占位符必须是实际目录,它想要检查规范文件路径是否在类路径上。
package alacs
import scala.language.experimental.macros
import scala.reflect.macros.Context
import scala.sys.process._
import java.io.File
/** A require macro to dynamically fudge the compilation classpath. */
object PathMaker {
// special place to unpack required libs, must be on the initial classpath
val entry = "required"
// whether to report updated syms without overly verbose -verbose
val talky = true
def require(c: Context)(name: c.Expr[String]): c.Expr[Unit] = {
import c.universe._
val st = c.universe.asInstanceOf[scala.reflect.internal.SymbolTable]
if (st.isCompilerUniverse) {
val Literal(Constant(what: String)) = name.tree
if (update(what)) {
val global = st.asInstanceOf[scala.tools.nsc.Global]
val (updated, _) = global invalidateClassPathEntries entry
c.info(c.enclosingPosition, s"Updated symbols $updated", force = talky)
} else {
c.abort(c.enclosingPosition, s"Couldn't unpack '$what' into '$entry'")
}
}
reify { () }
}
// figure out where name is, and update the special class path entry
def update(name: String): Boolean = {
// Process doesn't parse the command, it just splits on space,
// something about working on Windows
//val status = s"sh -c \"mkdir $entry ; ( cd $entry ; jar xf ../$name )\"".!
// but Process can set cwd for you
val command = s"jar xf ../$name"
val status = Process(command, new File(entry)).!
(status == 0)
}
}
所需的 API:
package alacs
import scala.language.experimental.macros
object Require {
def require(name: String): Unit = macro PathMaker.require
}
用法:
package sample
import alacs.Require._
/** Sample app requiring something not on the class path. */
object Test extends App {
require("special.jar")
import special._
Console println Special(7, "seven")
}
包装在 special.jar 中的东西
package special
case class Special(i: Int, s: String)
以这种方式测试:
rm -rf required
mkdir required
skalac pathmaker.scala
skalac -cp .:required require.scala sample.scala
skala -cp .:special.jar sample.Test
apm@mara:~/tmp/pathmaker$ . ./b
Unpack special.jar
sample.scala:8: Updated symbols List(package special)
require("special.jar")
^
Special(7,seven)
宏不会将其潜入运行时路径,这是脚本要做的事情。
但我认为 require 宏可以做一些方便的事情,比如有条件地抓取不同版本的 jar,具有不同的编译时特性(常量等)。
更新,只是验证它很疯狂:
require("fast.jar")
import constants._
Console println speed
require("slow.jar")
Console println speed
在哪里
package object constants {
//final val speed = 55
final val speed = 85
}
$ skalac -d fast.jar constants.scala
并使用内联常量运行它
85
55
新的警告:这是我的第一个宏,我正在为另一个应用程序寻找 invalidateClassPathEntries,所以我还没有探索限制。
更新:一个限制是控制宏何时展开。我想展示一些东西是如何针对旧 api 和新 api 进行编译的,并且必须将代码包装在块中以确保符号在需要之前可用:
require("oldfoo.jar")
locally {
import foo._
// something
require("newfoo.jar")
// try again
}
老警告:对不起,我知道你不遵守这个模糊的答案;我稍后会尝试,但与此同时,也许有人会清楚地向前迈进。
以前,我已经连接到编译器全局的“平台”实现,但对于这个用例来说,这可能是过度的。那时,您可以对类路径做任何您想做的事情,但我认为您想要一些开箱即用的东西。