diff --git a/clash-shake.cabal b/clash-shake.cabal
--- a/clash-shake.cabal
+++ b/clash-shake.cabal
@@ -1,15 +1,15 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.35.0.
+-- This file has been generated from package.yaml by hpack version 0.37.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           clash-shake
-version:        0.3.2
+version:        0.4.0
 synopsis:       Shake rules for building Clash programs
 description:    Shake rules for building Clash programs and synthesizing FPGA
                 configuration. Contains build rules for Xilinx ISE, Xilinx Vivado,
-                Intel Quartus and the open-source SymbiFlow toolchains.  See
+                Intel Quartus, F4PGA for Xilinx, and Yosys for ECP-5 and toolchains.  See
                 <https://github.com/gergoerdi/clash-pong/> for an example project.
 category:       Hardware, Shake
 homepage:       https://github.com/gergoerdi/clash-shake#readme
@@ -38,21 +38,22 @@
       Clash.Shake.Intel
       Clash.Shake.F4PGA
       Clash.Shake.SymbiFlow
+      Clash.Shake.ECP5
   other-modules:
       Paths_clash_shake
   hs-source-dirs:
       src
   build-depends:
-      aeson
+      aeson <3
     , base >=4.14 && <5
-    , bytestring
-    , clash-ghc >=1.4.2 && <1.5 || >=1.6.1 && <1.7
-    , clash-lib >=1.4.2 && <1.5 || >=1.6.1 && <1.7
-    , clash-prelude >=1.4.2 && <1.5 || >=1.6.1 && <1.7
-    , directory
-    , shake
-    , split
-    , stache
-    , text
-    , unordered-containers
+    , bytestring <0.13
+    , clash-ghc >=1.4.2 && <2.0
+    , clash-lib >=1.4.2 && <2.0
+    , clash-prelude >=1.4.2 && <2.0
+    , directory <2
+    , shake <0.20
+    , split <0.3
+    , stache <=3
+    , text <3
+    , unordered-containers <=1
   default-language: Haskell2010
diff --git a/src/Clash/Shake.hs b/src/Clash/Shake.hs
--- a/src/Clash/Shake.hs
+++ b/src/Clash/Shake.hs
@@ -8,6 +8,9 @@
     , RunClash(..), ClashKit(..)
     , clashRules
     , SynthKit(..)
+    , findFiles
+    , staticFiles
+    , SynthRules
 
     , binImage
 
@@ -140,6 +143,11 @@
     , phonies :: [(String, Action ())]
     }
 
+type SynthRules = ClashKit -> FilePath -> String -> Action [FilePath] -> Rules SynthKit
+
+findFiles :: [FilePath] -> [FilePattern] -> [FilePath]
+findFiles universe pats = filter (\fn -> (?== fn) `any` pats) universe
+
 nestedPhony :: String -> String -> Action () -> Rules ()
 nestedPhony target name = phony (target <> ":" <> name)
 
@@ -195,3 +203,9 @@
 withTargets targets rules
   | null targets = rules
   | otherwise = want targets >> withoutActions rules
+
+staticFiles :: FilePath -> Action [FilePath]
+staticFiles dir = do
+    files <- map (dir </>) <$> getDirectoryFiles dir ["//*"]
+    need files
+    return files
diff --git a/src/Clash/Shake/ECP5.hs b/src/Clash/Shake/ECP5.hs
new file mode 100644
--- /dev/null
+++ b/src/Clash/Shake/ECP5.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards, TemplateHaskell #-}
+module Clash.Shake.ECP5
+    ( ecp5
+    , fujprog
+    ) where
+
+import Clash.Shake
+
+import Development.Shake
+import Development.Shake.Command
+import Development.Shake.FilePath
+import Development.Shake.Config
+import Text.Printf
+
+ecp5 :: String -> SynthRules
+ecp5 device kit@ClashKit{..} outDir topName extraGenerated = do
+    let yosys :: String -> [String] -> Action ()
+        yosys tool args = cmd_ (EchoStdout False) =<< toolchain "YOSYS" tool args
+
+    let json = outDir </> topName <.> "json"
+
+    outDir </> topName <.> "ys" %> \out -> do
+        extraFiles <- findFiles <$> extraGenerated
+        srcs <- manifestSrcs
+        let verilogs = extraFiles ["//*.v"]
+        need $ srcs <> verilogs
+        writeFileChanged out $ unlines
+            [ printf "read_verilog %s" $ unwords (srcs <> verilogs)
+            , printf "hierarchy -top %s" topName
+            , printf "synth_ecp5 -json %s" json
+            ]
+
+    outDir <//> "*.json" %> \out -> do
+        extraFiles <- findFiles <$> extraGenerated
+        srcs <- manifestSrcs
+        let verilogs = extraFiles ["//*.v"]
+        need $ srcs <> verilogs
+
+        let ys = out -<.> "ys"
+        need [ys]
+        yosys "yosys" ["-q", ys]
+
+    outDir <//> "*.config" %> \out -> do
+        extraFiles <- findFiles <$> extraGenerated
+        let [lpf] = extraFiles ["//*.lpf"]
+        let json = out -<.> "json"
+
+        need [lpf, json]
+        yosys "nextpnr-ecp5" ["--json", json, "--textcfg", out, "--" <> device, "--lpf", lpf]
+
+    outDir <//> "*.bit" %> \out -> do
+        let config = out -<.> "config"
+        need [config]
+
+        yosys "ecppack" ["--input", config, "--bit", out]
+
+    let bitfile = outDir </> topName <.> "bit"
+
+    pure SynthKit
+        { bitfile = bitfile
+        , phonies =
+            [ "upload" |> fujprog [] bitfile
+            ]
+        }
+
+fujprog :: [String] -> FilePath -> Action ()
+fujprog args bitfile = do
+    need [bitfile]
+    cmd_ =<< toolchain "FUJPROG" "fujprog" (args ++ [bitfile])
diff --git a/src/Clash/Shake/F4PGA.hs b/src/Clash/Shake/F4PGA.hs
--- a/src/Clash/Shake/F4PGA.hs
+++ b/src/Clash/Shake/F4PGA.hs
@@ -12,23 +12,20 @@
 import Development.Shake.FilePath
 import Development.Shake.Config
 
-xilinx7 :: Xilinx.Board -> ClashKit -> FilePath -> FilePath -> String -> Rules SynthKit
-xilinx7 Xilinx.Board{ boardTarget = target@Xilinx.Target{..} } kit@ClashKit{..} outDir srcDir topName = do
+xilinx7 :: Xilinx.Board -> SynthRules
+xilinx7 Xilinx.Board{ boardTarget = target@Xilinx.Target{..} } kit@ClashKit{..} outDir topName extraGenerated = do
     let rootDir = joinPath . map (const "..") . splitPath $ outDir
 
     let symbiflow' :: String -> [String] -> Action ()
-        symbiflow' tool args = cmd_ (EchoStdout False) (Cwd outDir) =<< toolchain "SYMBIFLOW" tool args
+        symbiflow' tool args = cmd_ (EchoStdout False) (Cwd outDir) =<< toolchain "F4PGA" tool args
         symbiflow :: String -> [String] -> Action ()
-        symbiflow tool args = cmd_ (EchoStdout False) =<< toolchain "SYMBIFLOW" tool args
-
-    let getFiles dir pats = map (srcDir </>) <$> getDirectoryFiles srcDir [ dir </> pat | pat <- pats ]
-        verilogSrcs = getFiles "src-hdl" ["*.v"]
-        xdcSrcs = getFiles "src-hdl" ["*.xdc" ]
+        symbiflow tool args = cmd_ (EchoStdout False) =<< toolchain "F4PGA" tool args
 
     outDir </> topName <.> "eblif" %> \out -> do
+        extraFiles <- findFiles <$> extraGenerated
         srcs <- manifestSrcs
-        verilogs <- verilogSrcs
-        xdcs <- xdcSrcs
+        let verilogs = extraFiles ["//*.v"]
+            xdcs = extraFiles ["//*.xdc"]
         need $ srcs <> verilogs <> xdcs
 
         symbiflow' "symbiflow_synth" $
diff --git a/src/Clash/Shake/Intel.hs b/src/Clash/Shake/Intel.hs
--- a/src/Clash/Shake/Intel.hs
+++ b/src/Clash/Shake/Intel.hs
@@ -39,58 +39,38 @@
 arrowDeca :: Target
 arrowDeca = Target "MAX 10" "10M50DAF484C6GES"
 
-quartus :: Target -> ClashKit -> FilePath -> FilePath -> String -> Rules SynthKit
-quartus fpga kit@ClashKit{..} outDir srcDir topName = do
+quartus :: Target -> SynthRules
+quartus fpga kit@ClashKit{..} outDir topName extraGenerated = do
     let projectName = topName
         rootDir = joinPath . map (const "..") . splitPath $ outDir
 
     let quartus tool args = cmd_ (Cwd outDir) =<< toolchain "QUARTUS" tool args
 
-    let getFiles dir pats = getDirectoryFiles srcDir [ dir </> pat | pat <- pats ]
-        hdlSrcs = getFiles "src-hdl" ["*.vhdl", "*.v", "*.sv"]
-        tclSrcs = getFiles "src-hdl" ["*.tcl"]
-        constrSrcs = getFiles "src-hdl" ["*.sdc"]
-        ipCores = getFiles "ip" ["//*.qip"]
-
     outDir <//> "*.tcl" %> \out -> do
         srcs1 <- manifestSrcs
-        srcs2 <- hdlSrcs
-        tcls <- tclSrcs
-        constrs <- constrSrcs
-        cores <- ipCores
+        extraFiles <- findFiles <$> extraGenerated
+        let srcs2 = extraFiles ["//*.vhdl", "//*.v", "//*.sv"]
+            tcls = extraFiles ["//*.tcl"]
+            constrs = extraFiles ["//*.sdc"]
+            cores = extraFiles ["//*.qip"]
 
         let template = $(TH.compileMustacheFile "template/intel-quartus/project.tcl.mustache")
         let values = object . mconcat $
                 [ [ "project" .= T.pack projectName ]
                   , [ "top" .= T.pack topName ]
                   , targetMustache fpga
-                  , [ "srcs" .= mconcat
-                             [ [ object [ "fileName" .= (rootDir </> src) ] | src <- srcs1 ]
-                               , [ object [ "fileName" .= (rootDir </> srcDir </> src) ] | src <- srcs2 ]
-                             ]
+                  , [ "srcs" .= [ object [ "fileName" .= (rootDir </> src) ] | src <- srcs1 <> srcs2 ]
                     ]
-                  , [ "tclSrcs" .= [ object [ "fileName" .= (rootDir </> srcDir </> src) ] | src <- tcls ] ]
-                  , [ "ipcores" .= [ object [ "fileName" .= (rootDir </> srcDir </> core) ] | core <- cores ] ]
-                  , [ "constraintSrcs" .= [ object [ "fileName" .= (rootDir </> srcDir </> src) ] | src <- constrs ] ]
+                  , [ "tclSrcs" .= [ object [ "fileName" .= (rootDir </> src) ] | src <- tcls ] ]
+                  , [ "ipcores" .= [ object [ "fileName" .= (rootDir </> core) ] | core <- cores ] ]
+                  , [ "constraintSrcs" .= [ object [ "fileName" .= (rootDir </> src) ] | src <- constrs ] ]
                 ]
         writeFileChanged out . TL.unpack $ renderMustache template values
 
-    outDir </> "ip" <//> "*" %> \out -> do
-            let src = srcDir </> makeRelative outDir out
-            copyFileChanged src out
-
     let bitfile = outDir </> topName <.> "sof"
 
     bitfile %> \_out -> do
-        srcs1 <- manifestSrcs
-        srcs2 <- hdlSrcs
-        cores <- ipCores
-        need $ mconcat
-            [ [ outDir </> projectName <.> "tcl" ]
-              , [ src | src <- srcs1 ]
-              , [ srcDir </> src | src <- srcs2 ]
-              , [ outDir </> core | core <- cores ]
-            ]
+        need $ [ outDir </> projectName <.> "tcl" ]
         quartus "quartus_sh" ["-t", projectName <.> "tcl"]
 
     outDir </> topName <.> "rbf" %> \out -> do
diff --git a/src/Clash/Shake/Xilinx.hs b/src/Clash/Shake/Xilinx.hs
--- a/src/Clash/Shake/Xilinx.hs
+++ b/src/Clash/Shake/Xilinx.hs
@@ -82,21 +82,18 @@
 pynqZ2 = Board "tul.com.tw:pynq-z2:part0:1.0" 1 $
     Target "zynq7000" "xc7z020" "clg400" 1
 
-ise :: Target -> ClashKit -> FilePath -> FilePath -> String -> Rules SynthKit
-ise fpga kit@ClashKit{..} outDir srcDir topName = do
+ise :: Target -> SynthRules
+ise fpga kit@ClashKit{..} outDir topName extraGenerated = do
     let projectName = topName
         rootDir = joinPath . map (const "..") . splitPath $ outDir
 
     let ise tool args = cmd_ (Cwd outDir) =<< toolchain "ISE" tool args
 
-    let getFiles dir pats = getDirectoryFiles srcDir [ dir </> pat | pat <- pats ]
-        hdlSrcs = getFiles "src-hdl" ["*.vhdl", "*.v", "*.ucf" ]
-        ipCores = getFiles "ipcore_dir" ["*.xco", "*.xaw"]
-
     outDir <//> "*.tcl" %> \out -> do
         srcs1 <- manifestSrcs
-        srcs2 <- hdlSrcs
-        cores <- ipCores
+        extraFiles <- findFiles <$> extraGenerated
+        let srcs2 = extraFiles ["//*.vhdl", "//*.v", "//*.ucf"]
+            cores = extraFiles ["//*.xco", "//*.xaw"]
 
         let template = $(TH.compileMustacheFile "template/xilinx-ise/project.tcl.mustache")
         let values = object . mconcat $
@@ -104,9 +101,8 @@
                      , [ "top" .= T.pack topName ]
                      , targetMustache fpga
                      , [ "srcs" .= mconcat
-                         [ [ object [ "fileName" .= (rootDir </> src) ] | src <- srcs1 ]
-                         , [ object [ "fileName" .= (rootDir </> srcDir </> src) ] | src <- srcs2 ]
-                         , [ object [ "fileName" .= core ] | core <- cores ]
+                         [ [ object [ "fileName" .= (rootDir </> src) ] | src <- srcs1 <> srcs2 ]
+                         , [ object [ "fileName" .= ("ipcore_dir" </> takeFileName core) ] | core <- cores ]
                          ]
                        ]
                      , [ "ipcores" .= [ object [ "name" .= takeBaseName core ] | core <- cores ] ]
@@ -114,19 +110,20 @@
         writeFileChanged out . TL.unpack $ renderMustache template values
 
     outDir </> "ipcore_dir" <//> "*" %> \out -> do
-        let src = srcDir </> makeRelative outDir out
-        copyFileChanged src out
+        let src = makeRelative (outDir </> "ipcore_dir") out
+        extraFiles <- findFiles <$> extraGenerated
+        case extraFiles ["//" <> src] of
+          [oneFile] -> copyFileChanged oneFile out
+          [] -> error $ unwords ["Cannot find IP core file", src]
+          multiple ->  error $ unwords ["Multiple candidates for IP core file", src, show multiple]
 
     outDir </> topName <.> "bit" %> \_out -> do
-        srcs1 <- manifestSrcs
-        srcs2 <- hdlSrcs
-        cores <- ipCores
+        extraFiles <- findFiles <$> extraGenerated
+        let cores = extraFiles ["//*.xco", "//*.xaw"]
         need $ mconcat
-            [ [ outDir </> projectName <.> "tcl" ]
-            , [ src | src <- srcs1 ]
-            , [ srcDir </> src | src <- srcs2 ]
-            , [ outDir </> core | core <- cores ]
-            ]
+          [ [ outDir </> projectName <.> "tcl" ]
+          , [ outDir </> "ipcore_dir" </> takeFileName core | core <- cores ]
+          ]
         ise "xtclsh" [projectName <.> "tcl", "rebuild_project"]
 
     return $ SynthKit
@@ -138,8 +135,8 @@
             ]
         }
 
-vivado :: Board -> ClashKit -> FilePath -> FilePath -> String -> Rules SynthKit
-vivado board kit@ClashKit{..} outDir srcDir topName = do
+vivado :: Board -> SynthRules
+vivado board kit@ClashKit{..} outDir topName extraGenerated = do
     let projectName = topName
         projectDir = outDir </> projectName
         xpr = projectDir </> projectName <.> "xpr"
@@ -155,18 +152,14 @@
               , "-source", tcl
               ]
 
-    let getFiles dir pats = getDirectoryFiles srcDir [ dir </> pat | pat <- pats ]
-        hdlSrcs = getFiles "src-hdl" ["*.vhdl", "*.v" ]
-        constrSrcs = getFiles "src-hdl" ["*.xdc" ]
-        ipCores = getFiles "ip" ["*.xci"]
-
     xpr %> \out -> vivadoBatch "project.tcl"
 
     outDir </> "project.tcl" %> \out -> do
         srcs1 <- manifestSrcs
-        srcs2 <- hdlSrcs
-        cores <- ipCores
-        constrs <- constrSrcs
+        extraFiles <- findFiles <$> extraGenerated
+        let srcs2 = extraFiles ["//*.vhdl", "//*.v" ]
+            cores = extraFiles ["//*.xci"]
+            constrs = extraFiles ["//*.xdc"]
 
         let template = $(TH.compileMustacheFile "template/xilinx-vivado/project.tcl.mustache")
         let values = object . mconcat $
@@ -175,17 +168,16 @@
                      , [ "top" .= T.pack topName ]
                      , boardMustache board
                      , [ "srcs" .= mconcat
-                         [ [ object [ "fileName" .= src ] | src <- srcs1 ]
-                         , [ object [ "fileName" .= (srcDir </> src) ] | src <- srcs2 ]
+                         [ [ object [ "fileName" .= src ] | src <- srcs1 <> srcs2 ]
                          ]
                        ]
                      , [ "coreSrcs" .= object
                          [ "nonempty" .= not (null cores)
-                         , "items" .= [ object [ "fileName" .= (srcDir </> core) ] | core <- cores ]
+                         , "items" .= [ object [ "fileName" .= core ] | core <- cores ]
                          ]
                        ]
                      , [ "ipcores" .= [ object [ "name" .= takeBaseName core ] | core <- cores ] ]
-                     , [ "constraintSrcs" .= [ object [ "fileName" .= (srcDir </> src) ] | src <- constrs ] ]
+                     , [ "constraintSrcs" .= [ object [ "fileName" .= src ] | src <- constrs ] ]
                      ]
         writeFileChanged out . TL.unpack $ renderMustache template values
 
@@ -215,7 +207,7 @@
         , phonies =
             [ "vivado" |> do
                    need [xpr]
-                   vivado "vivado" [xpr]
+                   vivado "vivado" [makeRelative outDir xpr]
             , "upload" |> do
                    need [projectDir </> projectName <.> "runs" </> "impl_1" </> topName <.> "bit"]
                    vivadoBatch "upload.tcl"
