Main.hs 14.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
-- Copyright (c) 2013, Kenton Varda <temporal@gmail.com>
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are met:
--
-- 1. Redistributions of source code must retain the above copyright notice, this
--    list of conditions and the following disclaimer.
-- 2. 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.
--
-- 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 OWNER 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.

module Main ( main ) where

26
import System.Environment
27
import System.Console.GetOpt
28 29
import System.Exit(exitFailure, exitSuccess, ExitCode(..))
import System.IO(hPutStr, stderr, hSetBinaryMode, hClose)
Kenton Varda's avatar
Kenton Varda committed
30 31
import System.FilePath(takeDirectory)
import System.Directory(createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
32
import System.Entropy(getEntropy)
33
import System.Process(createProcess, proc, std_in, cwd, StdStream(CreatePipe), waitForProcess)
34
import Control.Monad
35
import Control.Monad.IO.Class(MonadIO, liftIO)
36
import Control.Exception(IOException, catch)
37 38
import Control.Monad.Trans.State(StateT, state, modify, evalStateT)
import qualified Control.Monad.Trans.State as State
39
import Prelude hiding (catch)
40
import Compiler
Kenton Varda's avatar
Kenton Varda committed
41 42 43 44
import Util(delimit)
import Text.Parsec.Pos
import Text.Parsec.Error
import Text.Printf(printf)
Kenton Varda's avatar
Kenton Varda committed
45
import qualified Data.List as List
46
import qualified Data.Map as Map
47
import qualified Data.ByteString.Lazy.Char8 as LZ
48
import Data.ByteString(unpack, pack, hPut)
49
import Data.Word(Word64, Word8)
50
import Data.Maybe(fromMaybe, catMaybes, mapMaybe)
51
import Data.Function(on)
52
import Semantics
53
import WireFormat(encodeSchema)
54
import CxxGenerator(generateCxx)
55 56
import Paths_capnproto_compiler
import Data.Version(showVersion)
57

58
type GeneratorFn = [FileDesc] -> [Word8] -> Map.Map Word64 [Word8] -> IO [(FilePath, LZ.ByteString)]
59

60
generatorFns :: Map.Map String GeneratorFn
61 62
generatorFns = Map.fromList [ ("c++", generateCxx) ]

Kenton Varda's avatar
Kenton Varda committed
63
data Opt = SearchPathOpt FilePath
64
         | OutputOpt String GeneratorFn FilePath
65
         | SrcPrefixOpt String
66 67
         | VerboseOpt
         | HelpOpt
68
         | VersionOpt
69
         | GenIdOpt
70 71

main :: IO ()
72
main = do
73
    let optionDescs =
Kenton Varda's avatar
Kenton Varda committed
74 75
         [ Option "I" ["import-path"] (ReqArg SearchPathOpt "DIR")
             "Search DIR for absolute imports."
76 77 78 79
         , Option "" ["src-prefix"] (ReqArg SrcPrefixOpt "PREFIX")
             "Prefix directory to strip off of source\n\
             \file names before generating output file\n\
             \names."
Kenton Varda's avatar
Kenton Varda committed
80
         , Option "o" ["output"] (ReqArg parseOutputArg "LANG[:DIR]")
81 82 83
             ("Generate output for language LANG\n\
              \to directory DIR (default: current\n\
              \directory).  LANG may be any of:\n\
84 85
              \  " ++ unwords (Map.keys generatorFns) ++ "\n\
              \or a plugin name.")
86
         , Option "v" ["verbose"] (NoArg VerboseOpt) "Write information about parsed files."
87
         , Option "i" ["generate-id"] (NoArg GenIdOpt) "Generate a new unique ID."
88
         , Option "h" ["help"] (NoArg HelpOpt) "Print usage info and exit."
89
         , Option "" ["version"] (NoArg VersionOpt) "Print version number and exit."
90 91 92 93 94
         ]
    let usage = usageInfo
         "capnpc [OPTION]... [FILE]...\n\
         \Generate source code based on Cap'n Proto definition FILEs.\n"
         optionDescs
95
    args <- getArgs
96
    let (options, files, errs) = getOpt Permute optionDescs args
97 98 99 100 101 102 103 104 105
    unless (null errs) (do
        mapM_ (hPutStr stderr) errs
        hPutStr stderr usage
        exitFailure)

    when (null options) (do
        hPutStr stderr "Nothing to do.\n"
        hPutStr stderr usage
        exitFailure)
106

107 108 109 110
    let isHelp = not $ null [opt | opt@HelpOpt <- options]
    when isHelp (do
        putStr usage
        exitSuccess)
111

112 113
    let isVersion = not $ null [opt | opt@VersionOpt <- options]
    when isVersion (do
114
        putStr ("Cap'n Proto Compiler " ++ showVersion Paths_capnproto_compiler.version ++ "\n")
115 116
        exitSuccess)

117 118 119 120 121 122
    let isGenId = not $ null [opt | opt@GenIdOpt <- options]
    when isGenId (do
        i <- generateId
        _ <- printf "@0x%016x\n" i
        exitSuccess)

123
    let isVerbose = not $ null [opt | opt@VerboseOpt <- options]
124
    let outputs = [(fn, dir) | OutputOpt _ fn dir <- options]
125 126 127 128 129 130

    -- TODO(someday):  We should perhaps determine the compiler binary's location and search its
    --   ../include as well.  Also, there should perhaps be a way to tell the compiler not to search
    --   these hard-coded default paths.
    let searchPath = ["/usr/local/include", "/usr/include"] ++
                     [dir | SearchPathOpt dir <- options]
131 132 133 134 135
        srcPrefixes = [addTrailingSlash prefix | SrcPrefixOpt prefix <- options]
        addTrailingSlash path =
            if not (null path) && last path /= '/'
                then path ++ "/"
                else path
136

137 138 139 140 141 142 143
    let verifyDirectoryExists dir = do
        exists <- doesDirectoryExist dir
        unless exists (do
            hPutStr stderr $ printf "no such directory: %s\n" dir
            exitFailure)
    mapM_ verifyDirectoryExists [dir | (_, dir) <- outputs]

144 145
    (failed, requestedFiles, allFiles) <-
        evalStateT (handleFiles isVerbose searchPath files)
Kenton Varda's avatar
Kenton Varda committed
146
                   (CompilerState False Map.empty)
147

148 149 150 151 152 153 154 155 156 157 158 159 160 161
    let (schema, schemaNodes) = encodeSchema requestedFiles allFiles
        toEntry ((i, _), node) = (i, node)
        schemaMap = Map.fromList $ map toEntry schemaNodes
        areDupes (i, _) (j, _) = i == j
        dupes = filter (\x -> length x > 1) $ List.groupBy areDupes
              $ List.sortBy (compare `on` fst) $ map fst schemaNodes

    unless (null dupes) (do
        hPutStr stderr $ concat
            ("Duplicate type / delcaration IDs detected:\n":
             map (concatMap (uncurry $ printf "  @0x%016x %s\n")) dupes)
        hPutStr stderr
            "IDs (16-digit hex strings prefixed with @0x) must be unique.  Sorry I'm not\n\
            \able to be more specific about where the duplicates were seen, but it should\n\
162
            \be easy enough to grep, right?\n"
163 164
        exitFailure)

165
    mapM_ (doOutput requestedFiles srcPrefixes schema schemaMap) outputs
166

167
    when failed exitFailure
168

169 170 171 172 173
handleFiles isVerbose searchPath files = do
    requestedFiles <- liftM catMaybes $ mapM (handleFile isVerbose searchPath) files
    CompilerState failed importMap <- State.get
    return (failed, requestedFiles, [ file | (_, ImportSucceeded file) <- Map.toList importMap ])

174
parseOutputArg :: String -> Opt
175 176 177 178 179 180 181 182 183 184
parseOutputArg str = let
    generatorFn lang wd = fromMaybe (callPlugin lang wd) $ Map.lookup lang generatorFns
    in case List.elemIndex ':' str of
        Just i -> let
            (lang, _:dir) = splitAt i str
            in OutputOpt lang (generatorFn lang (Just dir)) dir
        Nothing -> OutputOpt str (generatorFn str Nothing) "."

pluginName lang = if '/' `elem` lang then lang else "capnpc-" ++ lang

185
callPlugin lang wd _ schema _ = do
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    (Just hin, _, _, p) <- createProcess (proc (pluginName lang) [])
        { std_in = CreatePipe, cwd = wd }
    hSetBinaryMode hin True
    hPut hin (pack schema)
    hClose hin
    exitCode <- waitForProcess p
    case exitCode of
        ExitFailure 126 -> do
            _ <- printf "Plugin for language '%s' is not executable.\n" lang
            exitFailure
        ExitFailure 127 -> do
            _ <- printf "No plugin found for language '%s'.\n" lang
            exitFailure
        ExitFailure i -> do
            _ <- printf "Plugin for language '%s' failed with exit code: %d\n" lang i
            exitFailure
        ExitSuccess -> return []
203

Kenton Varda's avatar
Kenton Varda committed
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
-- As always, here I am, writing my own path manipulation routines, because the ones in the
-- standard lib don't do what I want.
canonicalizePath :: [String] -> [String]
-- An empty string anywhere other than the beginning must be caused by multiple consecutive /'s.
canonicalizePath (a:"":rest) = canonicalizePath (a:rest)
-- An empty string at the beginning means this is an absolute path.
canonicalizePath ("":rest) = "":canonicalizePath rest
-- "." is redundant.
canonicalizePath (".":rest) = canonicalizePath rest
-- ".." at the beginning of the path refers to the parent of the root directory.  Arguably this
-- is illegal but let's at least make sure that "../../foo" doesn't canonicalize to "foo"!
canonicalizePath ("..":rest) = "..":canonicalizePath rest
-- ".." cancels out the previous path component.  Technically this does NOT match what the OS would
-- do in the presence of symlinks:  `foo/bar/..` is NOT `foo` if `bar` is a symlink.  But, in
-- practice, the user almost certainly wants symlinks to behave exactly the same as if the
-- directory had been copied into place.
canonicalizePath (_:"..":rest) = canonicalizePath rest
-- In all other cases, just proceed on.
canonicalizePath (a:rest) = a:canonicalizePath rest
-- All done.
canonicalizePath [] = []

splitPath = loop [] where
    loop part ('/':text) = List.reverse part : loop [] text
    loop part (c:text) = loop (c:part) text
    loop part [] = [List.reverse part]

relativePath from searchPath relative = let
    splitFrom = canonicalizePath $ splitPath from
    splitRelative = canonicalizePath $ splitPath relative
    splitSearchPath = map splitPath searchPath
    -- TODO:  Should we explicitly disallow "/../foo"?
    resultPath = if head splitRelative == ""
        then map (++ tail splitRelative) splitSearchPath
        else [canonicalizePath (init splitFrom ++ splitRelative)]
    in map (List.intercalate "/") resultPath

firstExisting :: [FilePath] -> IO (Maybe FilePath)
firstExisting paths = do
    bools <- mapM doesFileExist paths
    let existing = [path | (True, path) <- zip bools paths]
    return (if null existing then Nothing else Just (head existing))

247 248 249 250 251
data ImportState = ImportInProgress | ImportFailed | ImportSucceeded FileDesc
type ImportStateMap = Map.Map String ImportState
data CompilerState = CompilerState Bool ImportStateMap
type CompilerMonad a = StateT CompilerState IO a

Kenton Varda's avatar
Kenton Varda committed
252 253
importFile :: Bool -> [FilePath] -> FilePath -> CompilerMonad (Either FileDesc String)
importFile isVerbose searchPath filename = do
254 255 256 257 258 259 260 261 262
    fileState <- state (\s@(CompilerState f m) -> case Map.lookup filename m of
        d@Nothing -> (d, CompilerState f (Map.insert filename ImportInProgress m))
        d -> (d, s))

    case fileState of
        Just ImportFailed -> return $ Right "File contained errors."
        Just ImportInProgress -> return $ Right "File cyclically imports itself."
        Just (ImportSucceeded d) -> return $ Left d
        Nothing -> do
Kenton Varda's avatar
Kenton Varda committed
263
            result <- readAndParseFile isVerbose searchPath filename
264 265 266 267 268
            modify (\(CompilerState f m) -> case result of
                Left desc -> CompilerState f (Map.insert filename (ImportSucceeded desc) m)
                Right _ -> CompilerState True (Map.insert filename ImportFailed m))
            return result

Kenton Varda's avatar
Kenton Varda committed
269
readAndParseFile isVerbose searchPath filename = do
270 271 272 273 274
    textOrError <- liftIO $ catch (fmap Left $ readFile filename)
        (\ex -> return $ Right $ show (ex :: IOException))

    case textOrError of
        Right err -> return $ Right err
Kenton Varda's avatar
Kenton Varda committed
275
        Left text -> parseFile isVerbose searchPath filename text
276

277 278 279 280 281 282 283 284 285 286
generateId :: MonadIO m => m Word64
generateId = do
    byteString <- liftIO $ getEntropy 8
    let i | ix < 2^(63::Integer) = ix + 2^(63::Integer)
          | otherwise = ix
        ix = foldl addByte 0 (unpack byteString)
        addByte :: Word64 -> Word8 -> Word64
        addByte b v = b * 256 + fromIntegral v
    return i

Kenton Varda's avatar
Kenton Varda committed
287
parseFile isVerbose searchPath filename text = do
288
    let importCallback name = do
Kenton Varda's avatar
Kenton Varda committed
289 290 291 292 293
            let candidates = relativePath filename searchPath name
            maybePath <- liftIO $ firstExisting candidates
            case maybePath of
                Nothing -> return $ Right "File not found."
                Just path -> importFile isVerbose searchPath path
294

295
    status <- parseAndCompileFile filename text importCallback generateId
296
    case status of
297
        Active desc [] -> do
298 299
            when isVerbose (liftIO $ print desc)
            return $ Left desc
300
        Active _ e -> do
301 302
            liftIO $ mapM_ printError (List.sortBy compareErrors e)
            return $ Right "File contained errors."
303
        Failed e -> do
304 305 306
            liftIO $ mapM_ printError (List.sortBy compareErrors e)
            return $ Right "File contained errors."

307 308
handleFile :: Bool -> [FilePath] -> FilePath -> CompilerMonad (Maybe FileDesc)
handleFile isVerbose searchPath filename = do
Kenton Varda's avatar
Kenton Varda committed
309
    result <- importFile isVerbose searchPath filename
310 311

    case result of
312 313 314
        Right e -> do
            liftIO $ hPutStr stderr (e ++ "\n")
            return Nothing
315 316
        Left desc -> return $ Just desc

317
doOutput requestedFiles srcPrefixes schema schemaMap output = do
318
    let write dir (name, content) = do
319 320 321 322
            let strippedOptions = mapMaybe (flip List.stripPrefix name) srcPrefixes
                stripped = if null strippedOptions then name else
                    List.minimumBy (compare `on` length) strippedOptions
                outFilename = dir ++ "/" ++ stripped
323 324 325
            createDirectoryIfMissing True $ takeDirectory outFilename
            LZ.writeFile outFilename content
        generate (generatorFn, dir) = do
326
            files <- generatorFn requestedFiles schema schemaMap
327 328
            mapM_ (write dir) files
    liftIO $ generate output
329

Kenton Varda's avatar
Kenton Varda committed
330
compareErrors a b = compare (errorPos a) (errorPos b)
Kenton Varda's avatar
Kenton Varda committed
331

Kenton Varda's avatar
Kenton Varda committed
332 333
-- TODO:  This is a fairly hacky way to make showErrorMessages' output not suck.  We could do better
--   by interpreting the error structure ourselves.
334
printError e = hPutStr stderr $ printf "%s:%d:%d: error: %s\n" f l c m' where
Kenton Varda's avatar
Kenton Varda committed
335 336 337 338 339 340
    pos = errorPos e
    f = sourceName pos
    l = sourceLine pos
    c = sourceColumn pos
    m = showErrorMessages "or" "Unknown parse error" "Expected" "Unexpected" "end of expression"
        (errorMessages e)
Kenton Varda's avatar
Kenton Varda committed
341
    m' = delimit "; " (List.filter (not . null) (lines m))