diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Gergő Érdi (http://gergo.erdi.hu/)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/clash-shake.cabal b/clash-shake.cabal
new file mode 100644
--- /dev/null
+++ b/clash-shake.cabal
@@ -0,0 +1,59 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 5f6e45bc2e072294d671a13e35cf938c8c9e4cfb32adee44eba24228b48bb9d6
+
+name:           clash-shake
+version:        0.1.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 and Xilinx Vivado
+                toolchains. See <https://github.com/gergoerdi/clash-pong/> for an
+                example project.
+category:       Hardware, Shake
+homepage:       https://github.com/gergoerdi/clash-shake#readme
+bug-reports:    https://github.com/gergoerdi/clash-shake/issues
+author:         Gergő Érdi
+maintainer:     gergo@erdi.hu
+copyright:      2021 Gergő Érdi
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    template/xilinx-ise/project.tcl.mustache
+    template/xilinx-vivado/project-build.tcl.mustache
+    template/xilinx-vivado/project.tcl.mustache
+    template/xilinx-vivado/upload.tcl.mustache
+
+source-repository head
+  type: git
+  location: https://github.com/gergoerdi/clash-shake
+
+library
+  exposed-modules:
+      Clash.Shake
+      Clash.Shake.Xilinx
+  other-modules:
+      Paths_clash_shake
+  hs-source-dirs:
+      src
+  build-depends:
+      aeson
+    , base >=4.14 && <5
+    , bytestring
+    , clash-ghc >=1.4.2 && <1.5
+    , clash-lib >=1.4.2 && <1.5
+    , clash-prelude >=1.4.2 && <1.5
+    , directory
+    , ghc-typelits-extra
+    , ghc-typelits-knownnat
+    , ghc-typelits-natnormalise
+    , shake
+    , split
+    , stache
+    , text
+    , unordered-containers
+  default-language: Haskell2010
diff --git a/src/Clash/Shake.hs b/src/Clash/Shake.hs
new file mode 100644
--- /dev/null
+++ b/src/Clash/Shake.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE RecordWildCards #-}
+module Clash.Shake
+    ( HDL(..)
+    , nestedPhony
+    , ClashKit(..)
+    , clashRules
+    , SynthKit(..)
+    , binImage
+    , useConfig
+    ) where
+
+import Development.Shake
+import Development.Shake.FilePath
+import Development.Shake.Config
+import Development.Shake.Util (parseMakefile)
+
+import qualified Clash.Main as Clash
+
+import Data.List.Split
+
+import qualified Data.Text as T
+import qualified Data.HashMap.Strict as HM
+import Data.Char (isUpper, toLower)
+import Control.Monad (forM_)
+import qualified Data.ByteString as BS
+import qualified System.Directory as Dir
+import Control.Exception (bracket)
+import Data.Maybe (fromJust)
+
+import Clash.Driver.Manifest
+import Clash.Prelude (pack)
+
+data HDL
+    = VHDL
+    | Verilog
+    | SystemVerilog
+    deriving (Eq, Enum, Bounded, Show, Read)
+
+hdlDir :: HDL -> FilePath
+hdlDir VHDL = "vhdl"
+hdlDir Verilog = "verilog"
+hdlDir SystemVerilog = "systemverilog"
+
+hdlExt :: HDL -> FilePath
+hdlExt VHDL = "vhdl"
+hdlExt Verilog = "v"
+hdlExt SystemVerilog = "sv"
+
+data ClashKit = ClashKit
+    { clash :: [String] -> Action ()
+    , manifestSrcs :: Action [FilePath]
+    }
+
+withWorkingDirectory :: FilePath -> IO a -> IO a
+withWorkingDirectory dir act =
+    bracket Dir.getCurrentDirectory Dir.setCurrentDirectory $ \_ ->
+        Dir.setCurrentDirectory dir >> act
+
+clashRules :: FilePath -> HDL -> [FilePath] -> FilePath -> [String] -> Action () -> Rules ClashKit
+clashRules outDir hdl srcDirs src clashFlags extraGenerated = do
+    let clash args = liftIO $ do
+            let srcFlags = ["-i" <> srcDir | srcDir <- srcDirs]
+            let args' = ["-outputdir", outDir] <> clashFlags <> srcFlags <> args
+            putStrLn $ "Clash.defaultMain " <> unwords args'
+            Clash.defaultMain args'
+
+    -- TODO: ideally, Clash should return the manifest, or at least its file location...
+    let synModule
+          | isModuleName src = src
+          | otherwise = "Main"
+
+        clashTopName = "topEntity"
+        synOut = outDir </> synModule <.> clashTopName
+        manifestFile = synOut </> "clash-manifest.json"
+        manifest = do
+            need [manifestFile]
+            Just manifest <- liftIO $ readManifest manifestFile
+            return manifest
+
+    let manifestSrcs = do
+            Manifest{..} <- manifest
+            let clashSrcs = map T.unpack componentNames <>
+                            [ map toLower clashTopName <> "_types" | hdl == VHDL ]
+            return [ synOut </> c <.> hdlExt hdl | c <- clashSrcs ]
+
+    getSrcs <- do
+        outDir </> "ghc-deps.make" %> \out -> do
+            alwaysRerun
+            -- By writing to a temp file and using `copyFileChanged`,
+            -- we avoid spurious reruns
+            -- (https://stackoverflow.com/a/64277431/477476)
+            withTempFileWithin outDir $ \tmp -> do
+                clash ["-M", "-dep-suffix", "", "-dep-makefile", tmp, src]
+                liftIO $ removeFiles outDir [takeBaseName tmp <.> "bak"]
+                copyFileChanged tmp out
+
+        return $ do
+            let depFile = outDir </> "ghc-deps.make"
+            need [depFile]
+            deps <- parseMakefile <$> liftIO (readFile depFile)
+            let isHsSource fn
+                  | ext `elem` [".hi"] = False
+                  | ext `elem` [".hs", ".lhs"] = True
+                  | otherwise = error $ "Unrecognized source file: " <> fn
+                  where
+                    ext = takeExtension fn
+                hsDeps = [fn | (_, fns) <- deps, fn <- fns, isHsSource fn]
+            return hsDeps
+
+    manifestFile %> \_out -> do
+        need =<< getSrcs
+        extraGenerated
+        clash [case hdl of { VHDL -> "--vhdl"; Verilog -> "--verilog"; SystemVerilog -> "--systemverilog" }, src]
+
+    return ClashKit{..}
+
+data SynthKit = SynthKit
+    { bitfile :: FilePath
+    , phonies :: [(String, Action ())]
+    }
+
+nestedPhony :: String -> String -> Action () -> Rules ()
+nestedPhony root name = phony (root </> name)
+
+useConfig :: FilePath -> Rules ()
+useConfig file = do
+    cfg <- do
+        haveConfig <- liftIO $ Dir.doesFileExist file
+        if haveConfig then do
+            usingConfigFile file
+            liftIO $ readConfigFile file
+          else do
+            usingConfig mempty
+            return mempty
+
+    forM_ (HM.lookup "TARGET" cfg) $ \target ->
+      want [target </> "bitfile"]
+
+binImage :: Maybe Int -> FilePath -> FilePath -> Action ()
+binImage size src out = do
+    need [src]
+    lines <- liftIO $ binLines size <$> BS.readFile src
+    writeFileChanged out (unlines lines)
+
+binLines :: Maybe Int -> BS.ByteString -> [String]
+binLines size bs = map (filter (/= '_') . show . pack) bytes
+  where
+    bytes = maybe id ensureSize size $ BS.unpack bs
+    ensureSize size bs = take size $ bs <> repeat 0x00
+
+isModuleName :: String -> Bool
+isModuleName = all (isUpper . head) . splitOn "."
diff --git a/src/Clash/Shake/Xilinx.hs b/src/Clash/Shake/Xilinx.hs
new file mode 100644
--- /dev/null
+++ b/src/Clash/Shake/Xilinx.hs
@@ -0,0 +1,203 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards, TemplateHaskell #-}
+module Clash.Shake.Xilinx
+    ( XilinxTarget(..), papilioPro, papilioOne, nexysA750T
+    , xilinxISE
+    , xilinxVivado
+    ) where
+
+import Clash.Shake
+
+import Development.Shake
+import Development.Shake.Command
+import Development.Shake.FilePath
+import Development.Shake.Config
+
+import Text.Mustache
+import qualified Text.Mustache.Compile.TH as TH
+import Data.Aeson
+
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.IO as T
+
+data XilinxTarget = XilinxTarget
+    { targetFamily :: String
+    , targetDevice :: String
+    , targetPackage :: String
+    , targetSpeed :: String
+    }
+
+targetMustache XilinxTarget{..} =
+    [ "targetFamily"  .= T.pack targetFamily
+    , "targetDevice"  .= T.pack targetDevice
+    , "targetPackage" .= T.pack targetPackage
+    , "targetSpeed"   .= T.pack targetSpeed
+    , "part"          .= T.pack (targetDevice <> targetPackage <> targetSpeed)
+    ]
+
+papilioPro :: XilinxTarget
+papilioPro = XilinxTarget "Spartan6" "xc6slx9" "tqg144" "-2"
+
+papilioOne :: XilinxTarget
+papilioOne = XilinxTarget "Spartan3E" "xc3s500e" "vq100" "-5"
+
+nexysA750T :: XilinxTarget
+nexysA750T = XilinxTarget "Artrix7" "xc7a50t" "icsg324" "-1L"
+
+xilinxISE :: XilinxTarget -> ClashKit -> FilePath -> FilePath -> String -> Rules SynthKit
+xilinxISE fpga kit@ClashKit{..} outDir srcDir topName = do
+    let projectName = topName
+        rootDir = joinPath . map (const "..") . splitPath $ outDir
+
+    let ise tool args = do
+            root <- getConfig "ISE_ROOT"
+            wrap <- getConfig "ISE"
+            let exe = case (wrap, root) of
+                    (Just wrap, _) -> [wrap, tool]
+                    (Nothing, Just root) -> [root </> "ISE/bin/lin64" </> tool]
+                    (Nothing, Nothing) -> error "ISE_ROOT or ISE must be set in build.mk"
+            cmd_ (Cwd outDir) exe 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
+
+        let template = $(TH.compileMustacheFile "template/xilinx-ise/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 ]
+                         , [ object [ "fileName" .= core ] | core <- cores ]
+                         ]
+                       ]
+                     , [ "ipcores" .= [ object [ "name" .= takeBaseName core ] | core <- cores ] ]
+                     ]
+        writeFileChanged out . TL.unpack $ renderMustache template values
+
+    outDir </> "ipcore_dir" <//> "*" %> \out -> do
+        let src = srcDir </> makeRelative outDir out
+        copyFileChanged src out
+
+    outDir </> topName <.> "bit" %> \_out -> do
+        srcs1 <- manifestSrcs
+        srcs2 <- hdlSrcs
+        cores <- ipCores
+        need $ mconcat
+            [ [ outDir </> projectName <.> "tcl" ]
+            , [ src | src <- srcs1 ]
+            , [ srcDir </> src | src <- srcs2 ]
+            , [ outDir </> core | core <- cores ]
+            ]
+        ise "xtclsh" [projectName <.> "tcl", "rebuild_project"]
+
+    return $ SynthKit
+        { bitfile = outDir </> topName <.> "bit"
+        , phonies =
+            [ "ise" |> do
+                   need [outDir </> projectName <.> "tcl"]
+                   ise "ise" [outDir </> projectName <.> "tcl"]
+            ]
+        }
+
+xilinxVivado :: XilinxTarget -> ClashKit -> FilePath -> FilePath -> String -> Rules SynthKit
+xilinxVivado fpga kit@ClashKit{..} outDir srcDir topName = do
+    let projectName = topName
+        projectDir = outDir </> projectName
+        xpr = projectDir </> projectName <.> "xpr"
+        rootDir = joinPath . map (const "..") . splitPath $ outDir
+
+    let vivado tool args = do
+            root <- getConfig "VIVADO_ROOT"
+            wrap <- getConfig "VIVADO"
+            let exe = case (wrap, root) of
+                    (Just wrap, _) -> [wrap, tool]
+                    (Nothing, Just root) -> [root </> "bin" </> tool]
+                    (Nothing, Nothing) -> error "VIVADO_ROOT or VIVADO must be set in build.mk"
+            cmd_ (Cwd outDir) exe args
+        vivadoBatch tcl = do
+            need [outDir </> tcl]
+            vivado "vivado"
+              [ "-mode", "batch"
+              , "-nojournal"
+              , "-nolog"
+              , "-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
+
+        let template = $(TH.compileMustacheFile "template/xilinx-vivado/project.tcl.mustache")
+        let values = object . mconcat $
+                     [ [ "rootDir" .= T.pack rootDir]
+                     , [ "project" .= T.pack projectName ]
+                     , [ "top" .= T.pack topName ]
+                     , targetMustache fpga
+                     , [ "board" .= T.pack "digilentinc.com:nexys-a7-50t:part0:1.0" ] -- TODO
+                     , [ "srcs" .= mconcat
+                         [ [ object [ "fileName" .= src ] | src <- srcs1 ]
+                         , [ object [ "fileName" .= (srcDir </> src) ] | src <- srcs2 ]
+                         ]
+                       ]
+                     , [ "coreSrcs" .= object
+                         [ "nonempty" .= not (null cores)
+                         , "items" .= [ object [ "fileName" .= (srcDir </> core) ] | core <- cores ]
+                         ]
+                       ]
+                     , [ "ipcores" .= [ object [ "name" .= takeBaseName core ] | core <- cores ] ]
+                     , [ "constraintSrcs" .= [ object [ "fileName" .= (srcDir </> src) ] | src <- constrs ] ]
+                     ]
+        writeFileChanged out . TL.unpack $ renderMustache template values
+
+    outDir </> "build.tcl" %> \out -> do
+        let template = $(TH.compileMustacheFile "template/xilinx-vivado/project-build.tcl.mustache")
+        let values = object . mconcat $
+                     [ [ "project" .= T.pack projectName ]
+                     , [ "top" .= T.pack topName ]
+                     ]
+        writeFileChanged out . TL.unpack $ renderMustache template values
+
+    outDir </> "upload.tcl" %> \out -> do
+        let template = $(TH.compileMustacheFile "template/xilinx-vivado/upload.tcl.mustache")
+        let values = object . mconcat $
+                     [ [ "project" .= T.pack projectName ]
+                     , [ "top" .= T.pack topName ]
+                     , targetMustache fpga
+                     ]
+        writeFileChanged out . TL.unpack $ renderMustache template values
+
+    projectDir </> projectName <.> "runs" </> "impl_1" </> topName <.> "bit" %> \out -> do
+        need [xpr]
+        vivadoBatch "build.tcl"
+
+    return SynthKit
+        { bitfile = projectDir </> projectName <.> "runs" </> "impl_1" </> topName <.> "bit"
+        , phonies =
+            [ "vivado" |> do
+                   need [xpr]
+                   vivado "vivado" [xpr]
+            , "upload" |> do
+                   need [projectDir </> projectName <.> "runs" </> "impl_1" </> topName <.> "bit"]
+                   vivadoBatch "upload.tcl"
+            ]
+        }
+
+(|>) :: String -> Action () -> (String, Action ())
+(|>) = (,)
diff --git a/template/xilinx-ise/project.tcl.mustache b/template/xilinx-ise/project.tcl.mustache
new file mode 100644
--- /dev/null
+++ b/template/xilinx-ise/project.tcl.mustache
@@ -0,0 +1,258 @@
+set myProject "{{project}}"
+set myScript "{{project}}.tcl"
+set myTop "{{top}}"
+
+proc run_process {} {
+
+   global myScript
+   global myProject
+
+   ## put out a 'heartbeat' - so we know something's happening.
+   puts "\n$myScript: running ($myProject)...\n"
+
+   if { ! [ open_project ] } {
+      return false
+   }
+
+   set_process_props
+   #
+   # Remove the comment characters (#'s) to enable the following commands
+   # process run "Synthesize"
+   # process run "Translate"
+   # process run "Map"
+   # process run "Place & Route"
+   #
+   set task "Implement Design"
+   if { ! [run_task $task] } {
+      puts "$myScript: $task run failed, check run output for details."
+      project close
+      return
+   }
+
+   set task "Generate Programming File"
+   if { ! [run_task $task] } {
+      puts "$myScript: $task run failed, check run output for details."
+      project close
+      return
+   }
+
+   puts "Run completed (successfully)."
+   project close
+
+}
+
+proc rebuild_project {} {
+
+   global myScript
+   global myProject
+
+   project close
+   ## put out a 'heartbeat' - so we know something's happening.
+   puts "\n$myScript: Rebuilding ($myProject)...\n"
+
+   set proj_exts [ list ise xise gise ]
+   foreach ext $proj_exts {
+      set proj_name "${myProject}.$ext"
+      if { [ file exists $proj_name ] } {
+         file delete $proj_name
+      }
+   }
+
+   project new $myProject
+   set_project_props
+   add_source_files
+   create_libraries
+   puts "$myScript: project rebuild completed."
+
+   run_process
+
+}
+
+proc run_task { task } {
+
+   # helper proc for run_process
+
+   puts "Running '$task'"
+   set result [ process run "$task" ]
+   #
+   # check process status (and result)
+   set status [ process get $task status ]
+   if { ( ( $status != "up_to_date" ) && \
+            ( $status != "warnings" ) ) || \
+         ! $result } {
+      return false
+   }
+   return true
+}
+
+proc show_help {} {
+
+   global myScript
+
+   puts ""
+   puts "usage: xtclsh $myScript <options>"
+   puts "       or you can run xtclsh and then enter 'source $myScript'."
+   puts ""
+   puts "options:"
+   puts "   run_process       - set properties and run processes."
+   puts "   rebuild_project   - rebuild the project from scratch and run processes."
+   puts "   set_project_props - set project properties (device, speed, etc.)"
+   puts "   add_source_files  - add source files"
+   puts "   create_libraries  - create vhdl libraries"
+   puts "   set_process_props - set process property values"
+   puts "   show_help         - print this message"
+   puts ""
+}
+
+proc open_project {} {
+
+   global myScript
+   global myProject
+
+   if { ! [ file exists ${myProject}.xise ] } {
+      ## project file isn't there, rebuild it.
+      puts "Project $myProject not found. Use project_rebuild to recreate it."
+      return false
+   }
+
+   project open $myProject
+
+   return true
+
+}
+
+proc set_project_props {} {
+
+   global myScript
+
+   if { ! [ open_project ] } {
+      return false
+   }
+
+   puts "$myScript: Setting project properties..."
+
+   project set family "{{targetFamily}}"
+   project set device "{{targetDevice}}"
+   project set package "{{targetPackage}}"
+   project set speed "{{targetSpeed}}"
+   project set top_level_module_type "HDL"
+   project set synthesis_tool "XST (VHDL/Verilog)"
+   project set simulator "ISim (VHDL/Verilog)"
+   project set "Preferred Language" "VHDL"
+}
+
+
+proc findRtfPath { relativePath } {
+   set xilenv ""
+   if { [info exists ::env(XILINX) ] } {
+      if { [info exists ::env(MYXILINX)] } {
+         set xilenv [join [list $::env(MYXILINX) $::env(XILINX)] $::xilinx::path_sep ]
+      } else {
+         set xilenv $::env(XILINX)
+      }
+   }
+   foreach path [ split $xilenv $::xilinx::path_sep ] {
+      set fullPath [ file join $path $relativePath ]
+      if { [ file exists $fullPath ] } {
+         return $fullPath
+      }
+   }
+   return ""
+}
+
+proc add_source_files {} {
+
+   global myProject
+   global myScript
+   global myTop
+
+   if { ! [ open_project ] } {
+      return false
+   }
+
+   source [ findRtfPath "data/projnav/scripts/dpm_cgUtils.tcl" ]
+
+   {{#ipcores}}
+   if { ! [ file exists "ipcore_dir/{{name}}.vhd" ] } {
+       puts "$myScript: Regenerating {{name}}"
+       cd ipcore_dir
+       run_cg_regen "{{name}}" {{targetDevice}}{{targetSpeed}}{{targetPackage}} VHDL CURRENT
+       cd ..
+   }
+   {{/ipcores}}
+
+
+   puts "$myScript: Adding sources to project..."
+
+   {{#srcs}}
+   xfile add "{{fileName}}"
+   {{/srcs}}
+
+   # Set the Top Module as well...
+   project set top "rtl" $myTop
+
+   puts "$myScript: project sources reloaded."
+
+} ; # end add_source_files
+
+proc create_libraries {} {
+
+   global myScript
+
+   if { ! [ open_project ] } {
+      return false
+   }
+
+   puts "$myScript: Creating libraries..."
+
+
+   # must close the project or library definitions aren't saved.
+   project save
+
+} ; # end create_libraries
+
+proc set_process_props {} {
+
+   global myScript
+
+   if { ! [ open_project ] } {
+      return false
+   }
+
+   puts "$myScript: setting process properties..."
+
+   project set "Compiled Library Directory" "\$XILINX/<language>/<simulator>"
+   project set "Functional Model Target Language" "VHDL" -process "View HDL Source"
+
+   puts "$myScript: project property values set."
+
+} ; # end set_process_props
+
+proc main {} {
+
+   if { [llength $::argv] == 0 } {
+      show_help
+      return true
+   }
+
+   foreach option $::argv {
+      switch $option {
+         "show_help"           { show_help }
+         "run_process"         { run_process }
+         "rebuild_project"     { rebuild_project }
+         "set_project_props"   { set_project_props }
+         "add_source_files"    { add_source_files }
+         "create_libraries"    { create_libraries }
+         "set_process_props"   { set_process_props }
+         default               { puts "unrecognized option: $option"; show_help }
+      }
+   }
+}
+
+if { $tcl_interactive } {
+   show_help
+} else {
+   if {[catch {main} result]} {
+      puts "$myScript failed: $result."
+   }
+}
diff --git a/template/xilinx-vivado/project-build.tcl.mustache b/template/xilinx-vivado/project-build.tcl.mustache
new file mode 100644
--- /dev/null
+++ b/template/xilinx-vivado/project-build.tcl.mustache
@@ -0,0 +1,9 @@
+open_project {{project}}/{{project}}.xpr
+update_compile_order -fileset sources_1
+reset_run synth_1
+launch_runs synth_1
+wait_on_run synth_1
+launch_runs impl_1
+wait_on_run impl_1
+launch_runs impl_1 -to_step write_bitstream
+wait_on_run impl_1
diff --git a/template/xilinx-vivado/project.tcl.mustache b/template/xilinx-vivado/project.tcl.mustache
new file mode 100644
--- /dev/null
+++ b/template/xilinx-vivado/project.tcl.mustache
@@ -0,0 +1,113 @@
+set origin_dir "{{rootDir}}"
+set _xil_proj_name_ "{{project}}"
+
+set orig_proj_dir "[file normalize "$origin_dir/"]"
+
+# Create project
+create_project ${_xil_proj_name_} ./${_xil_proj_name_} -force -part {{part}}
+
+# Set the directory path for the new project
+set proj_dir [get_property directory [current_project]]
+
+# Set project properties
+set obj [current_project]
+set_property -name "board_part" -value "{{board}}" -objects $obj
+set_property -name "default_lib" -value "xil_defaultlib" -objects $obj
+
+
+# Create 'sources_1' fileset (if not found)
+if {[string equal [get_filesets -quiet sources_1] ""]} {
+  create_fileset -srcset sources_1
+}
+set obj [get_filesets sources_1]
+
+set files [list \
+ {{#srcs}}
+ [file normalize "${origin_dir}/{{fileName}}" ]\
+ {{/srcs}}
+]
+add_files -norecurse -fileset $obj $files
+set_property -name "top" -value "{{top}}" -objects $obj
+
+{{#coreSrcs.nonempty}}
+set files [list \
+ {{#coreSrcs.items}}
+ [file normalize "${origin_dir}/{{fileName}}" ]\
+ {{/coreSrcs.items}}
+]
+set imported_files [import_files -fileset sources_1 $files]
+{{/coreSrcs.nonempty}}
+
+{{#ipcores}}
+set file "{{name}}/{{name}}.xci"
+set file_obj [get_files -of_objects [get_filesets sources_1] [list "*$file"]]
+set_property -name "generate_files_for_reference" -value "0" -objects $file_obj
+set_property -name "registered_with_manager" -value "1" -objects $file_obj
+if { ![get_property "is_locked" $file_obj] } {
+  set_property -name "synth_checkpoint_mode" -value "Singular" -objects $file_obj
+}
+{{/ipcores}}
+
+
+# Create 'constrs_1' fileset (if not found)
+if {[string equal [get_filesets -quiet constrs_1] ""]} {
+  create_fileset -constrset constrs_1
+}
+set obj [get_filesets constrs_1]
+
+{{#constraintSrcs}}
+set file [file normalize "${origin_dir}/{{fileName}}"]
+set file_imported [add_files -norecurse -fileset $obj $file]
+set file_obj [get_files -of_objects [get_filesets constrs_1] [list "*$file"]]
+set_property -name "file_type" -value "XDC" -objects $file_obj
+{{/constraintSrcs}}
+
+# Create 'sim_1' fileset (if not found)
+if {[string equal [get_filesets -quiet sim_1] ""]} {
+  create_fileset -simset sim_1
+}
+set obj [get_filesets sim_1]
+set_property -name "top" -value "{{top}}" -objects $obj
+set_property -name "top_lib" -value "xil_defaultlib" -objects $obj
+
+# Create 'synth_1' run (if not found)
+if {[string equal [get_runs -quiet synth_1] ""]} {
+    create_run -name synth_1 -part {{part}} -flow {Vivado Synthesis 2019} -strategy "Vivado Synthesis Defaults" -report_strategy {No Reports} -constrset constrs_1
+} else {
+  set_property strategy "Vivado Synthesis Defaults" [get_runs synth_1]
+  set_property flow "Vivado Synthesis 2019" [get_runs synth_1]
+}
+set obj [get_runs synth_1]
+set_property set_report_strategy_name 1 $obj
+set_property report_strategy {Vivado Synthesis Default Reports} $obj
+set_property set_report_strategy_name 0 $obj
+# Create 'synth_1_synth_report_utilization_0' report (if not found)
+if { [ string equal [get_report_configs -of_objects [get_runs synth_1] synth_1_synth_report_utilization_0] "" ] } {
+  create_report_config -report_name synth_1_synth_report_utilization_0 -report_type report_utilization:1.0 -steps synth_design -runs synth_1
+}
+set obj [get_report_configs -of_objects [get_runs synth_1] synth_1_synth_report_utilization_0]
+if { $obj != "" } {
+
+}
+set obj [get_runs synth_1]
+set_property -name "strategy" -value "Vivado Synthesis Defaults" -objects $obj
+
+# set the current synth run
+current_run -synthesis [get_runs synth_1]
+
+# Create 'impl_1' run (if not found)
+if {[string equal [get_runs -quiet impl_1] ""]} {
+    create_run -name impl_1 -part {{part}} -flow {Vivado Implementation 2019} -strategy "Vivado Implementation Defaults" -report_strategy {No Reports} -constrset constrs_1 -parent_run synth_1
+} else {
+  set_property strategy "Vivado Implementation Defaults" [get_runs impl_1]
+  set_property flow "Vivado Implementation 2019" [get_runs impl_1]
+}
+set obj [get_runs impl_1]
+set_property set_report_strategy_name 1 $obj
+set_property report_strategy {Vivado Implementation Default Reports} $obj
+set_property set_report_strategy_name 0 $obj
+
+# set the current impl run
+current_run -implementation [get_runs impl_1]
+
+puts "INFO: Project created:${_xil_proj_name_}"
diff --git a/template/xilinx-vivado/upload.tcl.mustache b/template/xilinx-vivado/upload.tcl.mustache
new file mode 100644
--- /dev/null
+++ b/template/xilinx-vivado/upload.tcl.mustache
@@ -0,0 +1,14 @@
+open_hw
+connect_hw_server
+open_hw_target
+
+set devs [get_hw_devices {{targetDevice}}_0]
+set dev [lindex $devs 0]
+
+current_hw_device $devs
+refresh_hw_device -update_hw_probes false $dev
+set_property PROGRAM.FILE {{project}}/{{project}}.runs/impl_1/{{top}}.bit $dev
+
+program_hw_devices $dev
+refresh_hw_device $dev
+
