dyre (empty) → 0.4
raw patch · 10 files changed
+626/−0 lines, 10 filesdep +Win32dep +basedep +directorysetup-changed
Dependencies added: Win32, base, directory, executable-path, filepath, ghc-paths, io-storage, old-time, process, unix, xdg-basedir
Files
- Config/Dyre.hs +149/−0
- Config/Dyre/Compat.hs +71/−0
- Config/Dyre/Compile.hs +58/−0
- Config/Dyre/Options.hs +115/−0
- Config/Dyre/Params.hs +40/−0
- Config/Dyre/Paths.hs +39/−0
- Config/Dyre/Relaunch.hs +76/−0
- LICENSE +28/−0
- Setup.hs +5/−0
- dyre.cabal +45/−0
+ Config/Dyre.hs view
@@ -0,0 +1,149 @@+{- |+Dyre is a library for configuring your Haskell programs. Like Xmonad,+programs configured with Dyre will look for a configuration file written+in Haskell, which essentially defines a custom program configured exactly+as the user wishes it to be. And since the configuration is written in+Haskell, the user is free to do anything they might wish in the context+of configuring the program.++Dyre places emphasis on elegance of operation and ease of integration+with existing applications. The 'wrapMain' function is the sole entry+point for Dyre. When partially applied with a parameter structure, it+wraps around the 'realMain' value from that structure, yielding an almost+identical function which has been augmented with dynamic recompilation+functionality.++The 'Config.Dyre.Relaunch' module provides the ability to restart the+program (recompiling if applicable), and persist state across restarts,+but it has no impact whatsoever on the rest of the library whether it+is used or not.++A full example of using Dyre's recompilation and relaunching features+is as follows:++>-- DyreExample.hs --+>module DyreExample where+>+>import qualified Config.Dyre as Dyre+>+>import Config.Dyre.Relaunch+>import System.IO+>+>data Config = Config { message :: String, errorMsg :: Maybe String }+>data State = State { bufferLines :: [String] } deriving (Read, Show)+>+>defaultConfig :: Config+>defaultConfig = Config "Dyre Example v0.1" Nothing+>+>showError :: Config -> String -> Config+>showError cfg msg = cfg { errorMsg = Just msg }+>+>realMain Config{message = message, errorMsg = errorMsg } = do+> (State buffer) <- restoreState $ State []+> case errorMsg of+> Nothing -> return ()+> Just em -> putStrLn $ "Error: " ++ em+> putStrLn message+> mapM putStrLn . reverse $ buffer+> putStr "> " >> hFlush stdout+> input <- getLine+> case input of+> "exit" -> return ()+> "quit" -> return ()+> other -> relaunchWithState (State $ other:buffer) Nothing+>+>dyreExample = Dyre.wrapMain $ Dyre.defaultParams+> { Dyre.projectName = "dyreExample"+> , Dyre.realMain = realMain+> , Dyre.showError = showError+> }++Notice that all of the program logic is contained in the 'DyreExample'+module. The main module of the program is absolutely trivial.++>-- Main.hs --+>module Main where+>import DyreExample+>main = dyreExample defaultConfig++When reading the above program, bear in mind exactly how much of the+code is simply *program logic*. Dyre is designed to intelligently+handle recompilation with a bare minimum of program modification.++Some mention should be made of Dyre's defaults. The 'defaultParams'+structure used in the example defines reasonable default values for+several configuration items. In the absence of any other definitions,+Dyre will default to outputting status messages to stderr, not hiding+any packages during compilation, and passing no special options to GHC.++Also, Dyre will expect configuration files to be placed at the path+'$XDG_CONFIG_HOME/<app>/<app>.hs', and it will store cache files in+the '$XDG_CACHE_HOME/<app>/' directory. The 'System.Environment.XDG'+module will be used to determine these paths, so refer to it for+behaviour on Windows platforms.+-}+module Config.Dyre ( wrapMain, Params(..), defaultParams ) where++import System.IO ( hPutStrLn, stderr )+import System.Directory ( doesFileExist )++import Config.Dyre.Params ( Params(..) )+import Config.Dyre.Compile ( customCompile )+import Config.Dyre.Compat ( customExec )+import Config.Dyre.Options ( getReconf, getDebug, withDyreOptions )+import Config.Dyre.Paths ( getPaths, maybeModTime )++-- | A set of reasonable defaults for configuring Dyre. If the minimal set of+-- fields are modified, the program will use the XDG-defined locations for+-- configuration and cache files (see 'System.Environment.XDG.BaseDir' for+-- details), pass no special options to GHC, and will output status messages+-- to stderr.+--+-- The fields that will have to be filled are 'projectName', 'realMain', and+-- 'showError'+defaultParams :: Params cfgType+defaultParams = Params+ { projectName = undefined+ , configDir = Nothing+ , cacheDir = Nothing+ , realMain = undefined+ , showError = undefined+ , hidePackages = []+ , ghcOpts = []+ , statusOut = hPutStrLn stderr+ }++-- | 'wrapMain' is how Dyre recieves control of the program. It is expected+-- that it will be partially applied with its parameters to yield a "main"+-- entry point, which will then be called by the 'main' function, as well+-- as by any custom configurations.+wrapMain :: Params cfgType -> cfgType -> IO ()+wrapMain params@Params{projectName = pName} cfg = withDyreOptions $ do+ -- Get the important paths+ (thisBinary, tempBinary, configFile, cacheDir) <- getPaths params++ -- Check their modification times+ thisTime <- maybeModTime thisBinary+ tempTime <- maybeModTime tempBinary+ confTime <- maybeModTime configFile++ -- If there's a config file, and the temp binary is older than something+ -- else, or we were specially told to recompile, then we should recompile.+ let confExists = confTime /= Nothing+ forceReconf <- getReconf+ errors <- if confExists &&+ (tempTime < confTime || tempTime < thisTime || forceReconf)+ then customCompile params+ else return Nothing++ -- If there's a custom binary and we're not it, run it. Otherwise+ -- just launch the main function, reporting errors if appropriate.+ -- Also we don't want to use a custom binary if the conf file is+ -- gone.+ customExists <- doesFileExist tempBinary+ if confExists && customExists && (thisBinary /= tempBinary)+ then do statusOut params $ "Launching custom binary '" ++ tempBinary ++ "'\n"+ customExec tempBinary Nothing+ else realMain params $ case errors of+ Nothing -> cfg+ Just er -> (showError params) cfg er
+ Config/Dyre/Compat.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ForeignFunctionInterface #-}++{- |+Compatibility code for things that need to be done differently+on different systems.+-}+module Config.Dyre.Compat ( customExec, getPIDString ) where++import Config.Dyre.Options ( customOptions )++#if defined(mingw32_HOST_OS) || defined(__MINGW32__)++import System.Win32+import System.Process+import System.Exit+import System.Mem++-- This can be removed as soon as a 'getProcessID' function+-- gets added to 'System.Win32'+foreign import stdcall unsafe "winbase.h GetCurrentProcessId"+ c_GetCurrentProcessID :: IO DWORD+getPIDString = fmap show c_GetCurrentProcessID++customExec binary mArgs = do+ args <- customOptions mArgs+ -- This whole thing is a terrible, ugly hack. Since Windows+ -- is too braindead to provide an exec() system call for us+ -- to use, we simply create a new process that inherits+ -- the stdio handles.+ (_,_,_,child) <- createProcess $ CreateProcess+ { cmdspec = RawCommand binary args+ , cwd = Nothing+ , env = Nothing+ , std_in = Inherit+ , std_out = Inherit+ , std_err = Inherit+ , close_fds = True+ }+ -- Do some garbage collection in an optimistic attempt to+ -- offset some of the memory we waste here.+ performGC+ -- And to prevent terminal apps from losing IO, we have to+ -- sit around waiting for the child to exit.+ exitCode <- waitForProcess child+ case exitCode of+ ExitSuccess -> c_ExitProcess 0+ ExitFailure c -> c_ExitProcess (fromIntegral c)++foreign import stdcall unsafe "winbase.h ExitProcess"+ c_ExitProcess :: UINT -> IO ()++#else++import System.Posix.Process ( executeFile, getProcessID )++getPIDString = fmap show getProcessID++customExec binary mArgs = do+ args <- customOptions mArgs+ executeFile binary False args Nothing++#endif++-- | Called whenever execution needs to be transferred over to+-- a different binary.+customExec :: FilePath -> Maybe [String] -> IO ()++-- | What it says on the tin. Gets the current PID as a string.+-- Used to determine the name for the state file during restarts.+getPIDString :: IO String
+ Config/Dyre/Compile.hs view
@@ -0,0 +1,58 @@+{- |+Compiling the custom executable. The majority of the code actually+deals with error handling, and not the compilation itself /per se/.+-}+module Config.Dyre.Compile ( customCompile ) where++import System.IO ( openFile, hClose, IOMode(..) )+import System.Exit ( ExitCode(..) )+import System.Process ( runProcess, waitForProcess )+import System.FilePath ( (</>) )+import System.Directory ( getCurrentDirectory, doesFileExist+ , createDirectoryIfMissing )+import Control.Exception ( bracket )+import GHC.Paths ( ghc )++import Config.Dyre.Paths ( getPaths )+import Config.Dyre.Params ( Params(..) )++-- | Attempts to compile the configuration file. Will return a string+-- containing any compiler output.+customCompile :: Params cfgType -> IO (Maybe String)+customCompile params@Params{statusOut = output} = do+ (thisBinary, tempBinary, configFile, cacheDir) <- getPaths params+ output $ "Configuration '" ++ configFile ++ "' changed. Recompiling."+ createDirectoryIfMissing True cacheDir++ -- Compile occurs in here+ let errFile = cacheDir </> "errors.log"+ result <- bracket (openFile errFile WriteMode) hClose $ \errHandle -> do+ ghcOpts <- makeFlags params configFile tempBinary+ ghcProc <- runProcess ghc ghcOpts (Just cacheDir) Nothing+ Nothing Nothing (Just errHandle)+ waitForProcess ghcProc++ -- Display a helpful little status message+ if result /= ExitSuccess+ then output $ "Error occurred while loading configuration file."+ else output $ "Program reconfiguration successful."++ -- If the error file exists and actually has some contents, return+ -- 'Just' the error string. Otherwise return 'Nothing'.+ errorsExist <- doesFileExist errFile+ if not errorsExist+ then return Nothing+ else do errors <- readFile errFile+ if errors == ""+ then return Nothing+ else return . Just $ errors++-- | Assemble the arguments to GHC so everything compiles right.+makeFlags :: Params cfgType -> FilePath -> FilePath -> IO [String]+makeFlags Params{ghcOpts = flags, hidePackages = hides} cfgFile tmpFile = do+ currentDir <- getCurrentDirectory+ return . concat $ [ ["-v0", "-fforce-recomp", "-i" ++ currentDir]+ , prefix "-hide-package" hides, flags+ , ["--make", cfgFile, "-o", tmpFile]+ ]+ where prefix y xs = concat . map (\x -> [y,x]) $ xs
+ Config/Dyre/Options.hs view
@@ -0,0 +1,115 @@+{-+Handling for the command-line options that can be used to configure+Dyre. As of the last count, there are four of them, and more are+unlikely to be needed. The only one that a user should ever need to+use is the '--force-reconf' option, so the others all begin with+'--dyre-<option-name>'.++At the start of the program, before anything else occurs, the+'withDyreOptions' function is used to hide Dyre's command-line+options. They are loaded into the IO monad using the module+'System.IO.Storage'. This keeps them safely out of the way of+the user code and our own.++Later, when Dyre needs to access the options, it does so through+the accessor functions defined here. When it comes time to pass+control over to a new binary, it gets an argument list which+preserves the important flags with a call to 'customOptions'.+-}+module Config.Dyre.Options+ ( withDyreOptions+ , customOptions+ , getReconf+ , getDebug+ , getMasterBinary+ , getStatePersist+ ) where++import Data.List+import Data.Maybe+import System.IO.Storage+import System.Environment+import System.Environment.Executable++-- | Store Dyre's command-line options to the IO-Store "dyre",+-- and then execute the provided IO action with all Dyre's+-- options removed from the command-line arguments.+withDyreOptions :: IO a -> IO a+withDyreOptions action = withStore "dyre" $ do+ -- Pretty important+ args <- getArgs++ -- If the flag exists, it overrides the current file. Likewise,+ -- if it doesn't exist, we end up with the path to our current+ -- file. This seems like a sensible way to do it.+ this <- getExecutablePath+ putValue "dyre" "masterBinary" this+ storeFlag args "--dyre-master-binary=" "masterBinary"++ -- Load the other important arguments into IO storage.+ storeFlag args "--dyre-state-persist=" "persistState"+ putValue "dyre" "forceReconf" $ "--force-reconf" `elem` args+ putValue "dyre" "debugMode" $ "--dyre-debug" `elem` args++ -- We filter the arguments, so now Dyre's arguments 'vanish'+ withArgs (filterArgs args) action+ where filterArgs = filter $ not . prefixElem dyreArgs+ prefixElem xs = or . zipWith ($) (map isPrefixOf xs) . repeat++-- | Get the value of the '--force-reconf' flag, which is used+-- to force a recompile of the custom configuration.+getReconf :: IO Bool+getReconf = getDefaultValue "dyre" "forceReconf" False++-- | Get the value of the '--dyre-debug' flag, which is used+-- to debug a program without installation. Specifically,+-- it forces the application to use './cache/' as the cache+-- directory, and './' as the configuration directory.+getDebug :: IO Bool+getDebug = getDefaultValue "dyre" "debugMode" False++-- | Get the path to the master binary. This is set to the path of+-- the *current* binary unless the '--dyre-master-binary=' flag+-- is set. Obviously, we pass the '--dyre-master-binary=' flag to+-- the custom configured application from the master binary.+getMasterBinary :: IO (Maybe String)+getMasterBinary = getValue "dyre" "masterBinary"++-- | Get the path to a persistent state file. This is set only when+-- the '--dyre-state-persist=' flag is passed to the program. It+-- is used internally by 'Config.Dyre.Relaunch' to save and restore+-- state when relaunching the program.+getStatePersist :: IO (Maybe String)+getStatePersist = getValue "dyre" "persistState"++-- | Return the set of options which will be passed to another instance+-- of Dyre. Preserves the master binary, state file, and debug mode+-- flags, but doesn't pass along the forced-recompile flag. Can be+-- passed a set of other arguments to use, or it defaults to using+-- the current arguments when passed 'Nothing'.+customOptions :: Maybe [String] -> IO [String]+customOptions otherArgs = do+ masterPath <- getMasterBinary+ stateFile <- getStatePersist+ debugMode <- getDebug+ return . filter (not . null) $ fromMaybe [] otherArgs +++ [ if debugMode then "--dyre-debug" else ""+ , case stateFile of+ Nothing -> ""+ Just sf -> "--dyre-state-persist=" ++ sf+ , "--dyre-master-binary=" ++ (fromJust masterPath)+ ]++-- | Look for the given flag in the argument array, and store+-- its value under the given name if it exists.+storeFlag :: [String] -> String -> String -> IO ()+storeFlag args flag name+ | null match = return ()+ | otherwise = putValue "dyre" name $ drop (length flag) (head match)+ where match = filter (isPrefixOf flag) args++-- | The array of all arguments that Dyre recognizes. Used to+-- make sure none of them are visible past 'withDyreOptions'+dyreArgs :: [String]+dyreArgs = [ "--force-reconf", "--dyre-state-persist"+ , "--dyre-debug", "--dyre-master-binary" ]
+ Config/Dyre/Params.hs view
@@ -0,0 +1,40 @@+{- |+Defines the 'Params' datatype which Dyre uses to define all+program-specific configuration data. Shouldn't be imported+directly, as 'Config.Dyre' re-exports it.+-}+module Config.Dyre.Params ( Params(..) ) where++-- | This structure is how all kinds of useful data is fed into Dyre. Of+-- course, only the 'projectName', 'realMain', and 'showError' fields+-- are really necessary. By using the set of default values provided+-- as 'Config.Dyre.defaultParams', you can get all the benefits of+-- using Dyre to configure your program in only five or six lines of+-- code.+data Params cfgType = Params+ { projectName :: String+ -- ^ The name of the project. This needs to also be the name of+ -- the executable, and the name of the configuration file.+ , configDir :: Maybe (IO FilePath)+ -- ^ The directory to look for a configuration file in.+ , cacheDir :: Maybe (IO FilePath)+ -- ^ The directory to store build files in, including the final+ -- generated executable.+ , realMain :: cfgType -> IO ()+ -- ^ The main function of the program. When Dyre has completed+ -- all of its recompilation, it passes the configuration data+ -- to this function and gets out of the way.+ , showError :: cfgType -> String -> cfgType+ -- ^ This function is used to display error messages that occur+ -- during recompilation, by allowing the program to modify its+ -- initial configuration.+ , hidePackages :: [String]+ -- ^ Packages that need to be hidden during compilation+ , ghcOpts :: [String]+ -- ^ Miscellaneous GHC compilation settings go here+ , statusOut :: String -> IO ()+ -- ^ A status output function. Will be called with messages+ -- when Dyre recompiles or launches anything. A good value+ -- is 'hPutStrLn stderr', assuming there is no pressing+ -- reason to not put messages on stderr.+ }
+ Config/Dyre/Paths.hs view
@@ -0,0 +1,39 @@+module Config.Dyre.Paths where++import System.Info+import System.Time+import System.FilePath+import System.Directory+import System.Environment.Executable+import System.Environment.XDG.BaseDir++import Config.Dyre.Params+import Config.Dyre.Options++-- | Return the paths to, respectively, the current binary, the custom+-- binary, the config file, and the cache directory.+getPaths :: Params c -> IO (FilePath, FilePath, FilePath, FilePath)+getPaths params@Params{projectName = pName} = do+ thisBinary <- getExecutablePath+ debugMode <- getDebug+ cwd <- getCurrentDirectory+ cacheDir <- case (debugMode, cacheDir params) of+ (True, _ ) -> return $ cwd </> "cache"+ (False, Nothing) -> getUserCacheDir pName+ (False, Just cd) -> cd+ configDir <- case (debugMode, configDir params) of+ (True, _ ) -> return $ cwd+ (False, Nothing) -> getUserConfigDir pName+ (False, Just cd) -> cd+ let tempBinary = cacheDir </> pName ++ "-" ++ os ++ "-" ++ arch+ let configFile = configDir </> pName ++ ".hs"+ return $ (thisBinary, tempBinary, configFile, cacheDir)++-- | Check if a file exists. If it exists, return Just the modification+-- time. If it doesn't exist, return Nothing.+maybeModTime :: FilePath -> IO (Maybe ClockTime)+maybeModTime path = do+ fileExists <- doesFileExist path+ if fileExists+ then fmap Just $ getModificationTime path+ else return Nothing
+ Config/Dyre/Relaunch.hs view
@@ -0,0 +1,76 @@+{-+This is the only other module aside from 'Config.Dyre' which needs+to be imported specially. It contains functions for restarting the+program (which, usefully, will cause a recompile if the config has+been changed), as well as saving and restoring state across said+restarts.++The impossibly simple function arguments are a consequence of a+little cheating we do using the 'System.IO.Storage' library. Of+course, we can't use the stored data unless something else put+it there, so this module will probably explode horribly if used+outside of a program whose recompilation is managed by Dyre.+-}+module Config.Dyre.Relaunch+ ( relaunchWithState+ , restoreState+ , relaunchMaster+ , maybeRestoreState+ ) where++import Data.Maybe ( fromMaybe, fromJust )+import System.IO ( writeFile, readFile )+import System.IO.Error ( try )+import System.FilePath ( (</>) )+import System.Directory ( getTemporaryDirectory, removeFile )++import System.IO.Storage ( putValue, delValue )+import Config.Dyre.Options ( customOptions, getMasterBinary, getStatePersist )+import Config.Dyre.Compat ( customExec, getPIDString )++-- | Just relaunch the master binary. We don't have any important+-- state to worry about. (Or, like when 'relaunchWithState' calls+-- it, we're managing state on our own.)+relaunchMaster :: Maybe [String] -> IO ()+relaunchMaster otherArgs = do+ masterPath <- fmap fromJust getMasterBinary+ customExec masterPath otherArgs++-- | Relaunch the master binary, but first preserve the program+-- state so that we can use the 'restoreState' functions to get+-- it back again later. Since we're not trying to be fancy here,+-- we'll just use 'show' to write it out.+relaunchWithState :: Show a => a -> Maybe [String] -> IO ()+relaunchWithState state otherArgs = do+ -- Calculate the path to the state file+ pidString <- getPIDString+ tempDir <- getTemporaryDirectory+ let statePath = tempDir </> pidString ++ ".state"+ -- Write the state to the file and store the path+ writeFile statePath . show $ state+ putValue "dyre" "persistState" statePath+ -- Relaunch+ relaunchMaster otherArgs++-- | Restore state that was previously saved by the 'relaunchWithState'+-- function. If unsuccessful, it will simply return Nothing.+maybeRestoreState :: Read a => IO (Maybe a)+-- This could probably be a lot simpler if I grokked+-- monad transformers better.+maybeRestoreState = do+ statePath <- getStatePersist+ case statePath of+ Nothing -> return Nothing+ Just sp -> do+ stateData <- readFile sp+-- removeFile sp+ delValue "dyre" "persistState"+ result <- try $ readIO stateData+ case result of+ Left _ -> return $ Nothing+ Right v -> return $ Just v++-- | Restore state using the 'maybeRestoreState' function, but return+-- the provided default state if we can't get anything better.+restoreState :: Read a => a -> IO a+restoreState d = fmap (fromMaybe d) maybeRestoreState
+ LICENSE view
@@ -0,0 +1,28 @@+Copyright (c) 2009, Will Donnelly+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++ * Neither the name of the software nor the names of its contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED+OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,5 @@+#!/usr/bin/env runhaskell++import Distribution.Simple++main = defaultMain
+ dyre.cabal view
@@ -0,0 +1,45 @@+name: dyre+version: 0.4+category: Development+synopsis: Dynamic reconfiguration in Haskell++description: Dyre implements dynamic reconfiguration facilities after the+ style of Xmonad. Dyre aims to be as simple as possible without+ sacrificing features, and places an emphasis on simplicity of+ integration with an application.++ A full introduction with a complete example project can be found+ in the documentation for 'Config.Dyre'++homepage: http://github.com/willdonnelly/dyre+bug-reports: http://github.com/willdonnelly/dyre/issues+stability: alpha+author: Will Donnelly+maintainer: Will Donnelly <will.donnelly@gmail.com>+copyright: (c) 2009 Will Donnelly+license: BSD3+license-file: LICENSE++build-type: Simple+cabal-version: >= 1.6++library+ exposed-modules: Config.Dyre,+ Config.Dyre.Paths,+ Config.Dyre.Compat,+ Config.Dyre.Params,+ Config.Dyre.Options,+ Config.Dyre.Compile,+ Config.Dyre.Relaunch+ build-depends: base >= 4 && < 5, process, filepath,+ directory, ghc-paths, old-time,+ executable-path, xdg-basedir, io-storage++ if os(windows)+ build-depends: Win32+ else+ build-depends: unix++source-repository head+ type: git+ location: git://github.com/willdonnelly/dyre.git