从(操作 a)的内容动态生成规则
Dynamically generating Rules from the content of an (Action a)
我目前正在测试将我们的构建系统从 make 移植到 shake,但遇到了障碍:
给定以下项目结构:
static/a.js
static/b.coffee
build/a.js
build/b.js
也就是说,不同的输入扩展名映射到相同的输出扩展名,因此直接的 "build//*.js" %>
规则是行不通的。
我想尽可能避免使用优先级,并且编写一个临时构建规则来检查是否存在任何可能的输入感觉很笨拙(特别是因为这种情况也发生在其他文件类型上),所以我写了以下:
data StaticFileMapping a = StaticFileMapping String String (FilePath -> FilePath -> Action a)
staticInputs :: FilePath -> StaticFileMapping a -> Action [FilePath]
staticInputs dir (StaticFileMapping iExt _ _) = (findFiles (dir </> "static") [iExt])
staticInputToOutput :: StaticFileMapping a -> FilePath -> FilePath
staticInputToOutput (StaticFileMapping _ oExt _) = (remapDir ["build"]) . (-<.> oExt)
staticTargets :: FilePath -> StaticFileMapping a -> Action [FilePath]
staticTargets dir sfm = (map $ staticInputToOutput sfm) <$> staticInputs dir sfm
rules :: FilePath -> StaticFileMapping a -> Rules ()
rules dir sfm@(StaticFileMapping _ _ process) = join $ mconcat . (map buildInputRule) <$> staticInputs dir sfm
where buildInputRule :: FilePath -> Rules ()
buildInputRule input = (staticInputToOutput sfm input) %> (process input)
这样我就可以为每个输入类型(.coffee -> .js
、.svg -> .png
)等定义一个映射,只需少量代码即可实现每个输入类型的转换。它几乎有效。
但据我所知,如果不先将 Action
中的值扔掉,从 (Action a)
到 Rules _
似乎是不可能的。
是否有类型为 (Action a) -> (a -> Rules ()) -> Rules ()
或 (Action a) -> (Rules a)
的函数?我可以自己实现其中之一,还是需要修改库的代码?
还是这整个方法都是轻率的,我应该采取其他方法?
首先,使用 priority
是行不通的,因为它会静态地选择一个规则然后 运行s 它 - 它不会回溯。同样重要的是,Shake 不会 运行 任何 Action
操作来生成 Rules
(根据您建议的两个函数),因为 Action
可能会调用 need
在它自己定义或由另一个操作规则定义的 Rule
上,从而使那些 Action
调用的顺序可见。您可以添加 IO (Rules ()) -> Rules ()
,这可能足以满足您的想法(目录列表),但目前尚未公开(我有一个内部函数可以做到这一点)。
举几个示例方法,定义合理的命令来转换 .js
/.coffee
文件很有用:
cmdCoffee :: FilePath -> FilePath -> Action ()
cmdCoffee src out = do
need [src]
cmd "coffee-script-convertor" [src] [out]
cmdJavascript :: FilePath -> FilePath -> Action ()
cmdJavascript = copyFile'
方法一:使用doesFileExist
这将是我的标准方法,写如下:
"build/*.js" %> \out -> do
let srcJs = "static" </> dropDirectory1 out
let srcCf = srcJs -<.> "coffee"
b <- doesFileExist srcCf
if b then cmdCoffee srcCf out else cmdJavascript srcJs out
这准确地捕获了如果用户在目录中添加 .coffee
文件那么规则应该是 re运行 的依赖性。如果这是您的常见模式,您可以想象给 doesFileExist
加糖。您甚至可以从 StaticFileMapping
结构列表中驱动它(在 oExt
字段上执行 group
以在每个 oExt
上添加一个规则,而不是在每个 doesFileExists
上调用iExt
依次)。这种方法的一个优点是,如果你这样做 shake build/out.js
它不需要做目录列表,尽管成本可以忽略不计。
方法 2:在 调用 shake
之前列出文件
而不是写 main = shakeArgs ...
做:
import System.Directory.Extra(listFilesRecursive) -- from the "extra" package
main = do
files <- listFilesRecursive "static"
shakeArgs shakeOptions $ do
forM_ files $ \src -> case takeExtension src of
".js" -> do
let out = "build" </> takeDirectory1 src
want [out]
out %> \_ -> cmdJavascript src out
-- rules for all other types you care about
_ -> return ()
这里是在IO中操作获取文件列表,然后可以参考之前抓取的那个值来添加规则。添加 rulesIO :: IO (Rules ()) -> Rules ()
将允许您列出 shakeArgs
.
中的文件
方法 3:列出 规则中的文件
您可以定义文件名和输出之间的映射,从目录列表驱动:
buildJs :: Action (Map FilePath (Action ()))
buildJs = do
js <- getDirectoryFiles "static" ["*.js"]
cf <- getDirectoryFiles "static" ["*.coffee"]
return $ Map.fromList $
[("build" </> j, cmdJavascript ("static" </> j) ("build" </> j)) | j <- js] ++
[("build" </> c, cmdCoffee ("static" </> c) ("")) | c <- cf]
然后将其提升为一组规则:
action $ do
mpJs <- buildJs
need $ Map.keys mpJs
"//*.js" %> \out -> do
mpJs <- buildJs
mpJs Map.! out
但是,这会为我们构建的每个文件重新计算目录列表,因此我们应该缓存它并确保它只计算一次:
mpJs <- newCache $ \() -> buildJs
action $ do
mpJs <- mpJs ()
need $ Map.keys mpJs
"//*.js" %> \out -> do
mpJs <- mpJs ()
mpJs Map.! out
此解决方案可能最接近您的原始方法,但我发现它是最复杂的。
我目前正在测试将我们的构建系统从 make 移植到 shake,但遇到了障碍:
给定以下项目结构:
static/a.js
static/b.coffee
build/a.js
build/b.js
也就是说,不同的输入扩展名映射到相同的输出扩展名,因此直接的 "build//*.js" %>
规则是行不通的。
我想尽可能避免使用优先级,并且编写一个临时构建规则来检查是否存在任何可能的输入感觉很笨拙(特别是因为这种情况也发生在其他文件类型上),所以我写了以下:
data StaticFileMapping a = StaticFileMapping String String (FilePath -> FilePath -> Action a)
staticInputs :: FilePath -> StaticFileMapping a -> Action [FilePath]
staticInputs dir (StaticFileMapping iExt _ _) = (findFiles (dir </> "static") [iExt])
staticInputToOutput :: StaticFileMapping a -> FilePath -> FilePath
staticInputToOutput (StaticFileMapping _ oExt _) = (remapDir ["build"]) . (-<.> oExt)
staticTargets :: FilePath -> StaticFileMapping a -> Action [FilePath]
staticTargets dir sfm = (map $ staticInputToOutput sfm) <$> staticInputs dir sfm
rules :: FilePath -> StaticFileMapping a -> Rules ()
rules dir sfm@(StaticFileMapping _ _ process) = join $ mconcat . (map buildInputRule) <$> staticInputs dir sfm
where buildInputRule :: FilePath -> Rules ()
buildInputRule input = (staticInputToOutput sfm input) %> (process input)
这样我就可以为每个输入类型(.coffee -> .js
、.svg -> .png
)等定义一个映射,只需少量代码即可实现每个输入类型的转换。它几乎有效。
但据我所知,如果不先将 Action
中的值扔掉,从 (Action a)
到 Rules _
似乎是不可能的。
是否有类型为 (Action a) -> (a -> Rules ()) -> Rules ()
或 (Action a) -> (Rules a)
的函数?我可以自己实现其中之一,还是需要修改库的代码?
还是这整个方法都是轻率的,我应该采取其他方法?
首先,使用 priority
是行不通的,因为它会静态地选择一个规则然后 运行s 它 - 它不会回溯。同样重要的是,Shake 不会 运行 任何 Action
操作来生成 Rules
(根据您建议的两个函数),因为 Action
可能会调用 need
在它自己定义或由另一个操作规则定义的 Rule
上,从而使那些 Action
调用的顺序可见。您可以添加 IO (Rules ()) -> Rules ()
,这可能足以满足您的想法(目录列表),但目前尚未公开(我有一个内部函数可以做到这一点)。
举几个示例方法,定义合理的命令来转换 .js
/.coffee
文件很有用:
cmdCoffee :: FilePath -> FilePath -> Action ()
cmdCoffee src out = do
need [src]
cmd "coffee-script-convertor" [src] [out]
cmdJavascript :: FilePath -> FilePath -> Action ()
cmdJavascript = copyFile'
方法一:使用doesFileExist
这将是我的标准方法,写如下:
"build/*.js" %> \out -> do
let srcJs = "static" </> dropDirectory1 out
let srcCf = srcJs -<.> "coffee"
b <- doesFileExist srcCf
if b then cmdCoffee srcCf out else cmdJavascript srcJs out
这准确地捕获了如果用户在目录中添加 .coffee
文件那么规则应该是 re运行 的依赖性。如果这是您的常见模式,您可以想象给 doesFileExist
加糖。您甚至可以从 StaticFileMapping
结构列表中驱动它(在 oExt
字段上执行 group
以在每个 oExt
上添加一个规则,而不是在每个 doesFileExists
上调用iExt
依次)。这种方法的一个优点是,如果你这样做 shake build/out.js
它不需要做目录列表,尽管成本可以忽略不计。
方法 2:在 调用 shake
而不是写 main = shakeArgs ...
做:
import System.Directory.Extra(listFilesRecursive) -- from the "extra" package
main = do
files <- listFilesRecursive "static"
shakeArgs shakeOptions $ do
forM_ files $ \src -> case takeExtension src of
".js" -> do
let out = "build" </> takeDirectory1 src
want [out]
out %> \_ -> cmdJavascript src out
-- rules for all other types you care about
_ -> return ()
这里是在IO中操作获取文件列表,然后可以参考之前抓取的那个值来添加规则。添加 rulesIO :: IO (Rules ()) -> Rules ()
将允许您列出 shakeArgs
.
方法 3:列出 规则中的文件
您可以定义文件名和输出之间的映射,从目录列表驱动:
buildJs :: Action (Map FilePath (Action ()))
buildJs = do
js <- getDirectoryFiles "static" ["*.js"]
cf <- getDirectoryFiles "static" ["*.coffee"]
return $ Map.fromList $
[("build" </> j, cmdJavascript ("static" </> j) ("build" </> j)) | j <- js] ++
[("build" </> c, cmdCoffee ("static" </> c) ("")) | c <- cf]
然后将其提升为一组规则:
action $ do
mpJs <- buildJs
need $ Map.keys mpJs
"//*.js" %> \out -> do
mpJs <- buildJs
mpJs Map.! out
但是,这会为我们构建的每个文件重新计算目录列表,因此我们应该缓存它并确保它只计算一次:
mpJs <- newCache $ \() -> buildJs
action $ do
mpJs <- mpJs ()
need $ Map.keys mpJs
"//*.js" %> \out -> do
mpJs <- mpJs ()
mpJs Map.! out
此解决方案可能最接近您的原始方法,但我发现它是最复杂的。