urembed-0.5.0.0: src/Urembed.hs
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ImpredicativeTypes #-}
module Main where
import Control.Monad.Error
import Control.Monad.State
import Control.Monad.Writer
import Language.JavaScript.Parser
import System.Environment
import System.Process
import System.Exit
import System.IO
import System.FilePath
import System.Directory
import System.Info
import Network.Mime
import Text.Printf
import Data.Either
import Data.Generics
import Data.Char
import Data.List
import Data.Data
import Data.Typeable
import Data.Maybe
import Data.String
import qualified Data.Text as T
import qualified Data.ByteString.Char8 as BS
import qualified Development.Cake3 as C3
import Development.Cake3(runMake,makevar,cmd,rule,extvar,File(..),phony,depend)
import qualified Development.Cake3.Rules.UrWeb as C3
import Development.Cake3.Rules.UrWeb (Config(..), urdeps,defaultConfig)
import Options.Applicative
import Paths_urembed
io :: (MonadIO m) => IO a -> m a
io = liftIO
hio :: (MonadIO m) => Handle -> String -> m ()
hio h = io . hPutStrLn h
err,out :: (MonadIO m) => String -> m ()
err = hio stderr
out = hio stdout
span2 :: String -> String -> Maybe (String,String)
span2 inf s = span' [] s where
span' _ [] = Nothing
span' acc (c:cs)
| isPrefixOf inf (c:cs) = Just (acc, drop (length inf) (c:cs))
| otherwise = span' (acc++[c]) cs
data JSFunc = JSFunc {
urdecl :: String
, urname :: String
, jsname :: String
} deriving(Show)
data JSType = JSType {
urtdecl :: String
} deriving(Show)
-- | Parse the JavaScript file, extract top-level functions, convert their
-- signatures into Ur/Web format, return them as the list of strings
parse_js :: FilePath -> IO (Either String ([JSType],[JSFunc]))
parse_js file = do
s <- readFile file
runErrorT $ do
c <- either fail return (parse s file)
f <- concat <$> (forM (findTopLevelFunctions c) $ \f@(fn:_) -> (do
ts <- mapM extractEmbeddedType (f`zip`(False:repeat True))
let urdecl_ = urs_line ts
let urname_ = (fst (head ts))
let jsname_ = fn
return [JSFunc urdecl_ urname_ jsname_]
) `catchError` (\(e::String) -> do
err $ printf "ignoring function %s, reason:\n\t%s" fn e
return []))
t <- concat <$> (forM (findTopLevelVars c) $ \vn -> (do
(n,t) <- extractEmbeddedType (vn,False)
return [JSType $ printf "type %s" t]
)`catchError` (\(e::String) -> do
err $ printf "ignoring variable %s, reason:\n\t%s" vn e
return []))
return (t,f)
where
urs_line :: [(String,String)] -> String
urs_line [] = error "wrong function signature"
urs_line ((n,nt):args) = printf "val %s : %s" n (fmtargs args) where
fmtargs :: [(String,String)] -> String
fmtargs ((an,at):as) = printf "%s -> %s" at (fmtargs as)
fmtargs [] = let pf = stripPrefix "pure_" nt in
case pf of
Just p -> p
Nothing -> printf "transaction %s" nt
extractEmbeddedType :: (Monad m) => (String,Bool) -> m (String,String)
extractEmbeddedType ([],_) = error "BUG: empty identifier"
extractEmbeddedType (name,fallback) = check (msum [span2 "__" name , span2 "_as_" name]) where
check (Just (n,t)) = return (n,t)
check _ | fallback == True = return (name,name)
| fallback == False = fail $ printf "Can't extract the type from the identifier '%s'" name
findTopLevelFunctions :: JSNode -> [[String]]
findTopLevelFunctions top = map decls $ listify is_func top where
is_func n@(JSFunction a b c d e f) = True
is_func _ = False
decls (JSFunction a b c d e f) = (identifiers b) ++ (identifiers d)
findTopLevelVars :: JSNode -> [String]
findTopLevelVars top = map decls $ listify is_var top where
is_var n@(JSVarDecl a []) = True
is_var _ = False
decls (JSVarDecl a _) = (head $ identifiers a);
identifiers x = map name $ listify ids x where
ids i@(JSIdentifier s) = True
ids _ = False
name (JSIdentifier n) = n
data Args = A
{ tgtdir :: FilePath
, version :: Bool
, files :: [FilePath]
}
pargs :: Parser Args
pargs = A
<$> strOption
( long "output"
<> short 'o'
<> metavar "FILE.urp"
<> help "Name of the Ur/Web project being generated"
<> value "")
<*> flag False True ( long "version" <> help "Show version information" )
<*> arguments str ( metavar "FILE" <> help "File to embed" )
where
osdefgcc | isInfixOf "linux" os = "/usr/bin/gcc"
| isInfixOf "windows" os = "c:\\cygwin\\usr\\bin\\gcc"
| otherwise = "/usr/local/bin/gcc"
replaceExtensions f x = addExtension (dropExtensions f) x
f .= x = replaceExtensions f x
guessMime inf = fixup $ BS.unpack (defaultMimeLookup (fromString inf)) where
fixup "application/javascript" = "text/javascript"
fixup m = m
-- readBinaryFile name = BS.openBinaryFile name ReadMode >>= BS.hGetContents
main :: IO ()
main = do
h <- (getDataFileName >=> readFile) "Help.txt"
main_ =<< execParser (
info (helper <*> pargs)
( fullDesc
<> progDesc h
<> header "UrEmebed is the Ur/Web module generator" ))
main_ (A tgturp True ins) = do
hPutStrLn stderr "urembed version 0.5.0.0"
main_ (A tgturp False ins) = do
let tgtdir = takeDirectory tgturp
when (null tgtdir) $ do
fail "An output directory should be specified, use -o"
when (null ins) $ do
fail "At least one file should be specified, see --help"
exists <- doesDirectoryExist tgtdir
when (not exists) $ do
fail "Output is not a directory"
let indest n = tgtdir </> n
let write n wr = writeFile (indest n) $ execWriter $ wr
forM_ ins $ \inf -> do
hPutStrLn stderr (printf "Processing %s" inf)
let modname = (mkname inf)
let modname_c = modname ++ "_c"
let blobname = modname ++ "_c_blob"
let modname_js = modname ++ "_js"
let mime = guessMime inf
-- Module_c.urp
let binfunc = printf "uw_%s_binary" modname_c
let textfunc = printf "uw_%s_text" modname_c
write (replaceExtension modname_c ".urs") $ do
line $ "val binary : unit -> transaction blob"
line $ "val text : unit -> transaction string"
content <- liftIO $ BS.readFile inf
let csrc = replaceExtension modname_c ".c"
write csrc $ do
line $ "// Thanks, http://stupefydeveloper.blogspot.ru/2008/08/cc-embed-binary-data-into-elf.html"
line $ "#include <urweb.h>"
line $ "#include <stdio.h>"
-- let start = printf "_binary___%s_start" blobname
-- let size = printf "_binary___%s_size" blobname
line $ printf "#define BLOBSZ %d" (BS.length content)
line $ "static char blob[BLOBSZ];"
line $ "uw_Basis_blob " ++ binfunc ++ " (uw_context ctx, uw_unit unit)"
line $ "{"
line $ " uw_Basis_blob uwblob;"
line $ " uwblob.data = &blob[0];"
line $ " uwblob.size = BLOBSZ;"
line $ " return uwblob;"
line $ "}"
line $ ""
line $ "uw_Basis_string " ++ textfunc ++ " (uw_context ctx, uw_unit unit) {"
line $ " char* data = &blob[0];"
line $ " size_t size = sizeof(blob);"
line $ " char * c = uw_malloc(ctx, size+1);"
line $ " char * write = c;"
line $ " int i;"
line $ " for (i = 0; i < size; i++) {"
line $ " *write = data[i];"
line $ " if (*write == '\\0')"
line $ " *write = '\\n';"
line $ " *write++;"
line $ " }"
line $ " *write=0;"
line $ " return c;"
line $ " }"
line $ ""
let append n wr = BS.appendFile (indest n) $ execWriter $ wr
append csrc $ do
let line s = tell ((BS.pack s)`mappend`(BS.pack "\n"))
line $ ""
line $ "static char blob[BLOBSZ] = {"
let buf = reverse $ BS.foldl (\a c -> (BS.pack (printf "0x%02X ," c)) : a) [] content
tell (BS.concat buf)
line $ "};"
line $ ""
let header = (replaceExtension modname_c ".h")
write header $ do
line $ "#include <urweb.h>"
line $ "uw_Basis_blob " ++ binfunc ++ " (uw_context ctx, uw_unit unit);"
line $ "uw_Basis_string " ++ textfunc ++ " (uw_context ctx, uw_unit unit);"
let binobj = replaceExtension modname_c ".o"
-- let dataobj = replaceExtension modname_c ".data.o"
write (replaceExtension modname_c ".urp") $ do
line $ "ffi " ++ modname_c
line $ "include " ++ header
line $ "link " ++ binobj
-- line $ "link " ++ dataobj
-- Copy the file to the target dir and run linker from there. Thus the names
-- it places will be correct (see start,size in _c)
-- copyFile inf (indest blobname)
-- Module_js.urp
(jstypes,jsdecls) <- if ((takeExtension inf) == ".js") then do
e <- parse_js inf
case e of
Left e -> do
err $ printf "Error while parsing %s" (takeFileName inf)
fail e
Right decls -> do
-- err (show decls)
return decls
else
return ([],[])
write (replaceExtension modname_js ".urs") $ do
forM_ jstypes $ \decl -> line (urtdecl decl)
forM_ jsdecls $ \decl -> line (urdecl decl)
write (replaceExtension modname_js ".urp") $ do
line $ "ffi " ++ modname_js
forM_ jsdecls $ \decl -> do
line $ printf "jsFunc %s.%s = %s" modname_js (urname decl) (jsname decl)
line $ printf "benignEffectful %s.%s" modname_js (urname decl)
-- Module.urp
write (replaceExtension modname ".urs") $ do
line $ "val binary : unit -> transaction blob"
line $ "val text : unit -> transaction string"
line $ "val blobpage : unit -> transaction page"
line $ "val geturl : url"
forM_ jstypes $ \decl -> line (urtdecl decl)
forM_ jsdecls $ \d -> line (urdecl d)
write (replaceExtension modname ".ur") $ do
line $ "val binary = " ++ modname_c ++ ".binary"
line $ "val text = " ++ modname_c ++ ".text"
forM_ jsdecls $ \d ->
line $ printf "val %s = %s.%s" (urname d) modname_js (urname d)
line $ printf "fun blobpage {} = b <- binary () ; returnBlob b (blessMime \"%s\")" mime
line $ "val geturl = url(blobpage {})"
write (replaceExtension modname ".urp") $ do
line $ "library " ++ modname_c
line $ "library " ++ modname_js
line $ printf "safeGet %s/blobpage" modname
line $ printf "safeGet %s/blob" modname
line $ ""
line $ modname
-- Static.urp
let tgt_in = replaceExtensions tgturp ".urp.in"
writeFile tgt_in $ execWriter $ do
forM_ ins $ \inf -> do
line $ printf "library %s" (mkname inf)
line []
line (takeBaseName tgturp)
let datatype = execWriter $ do
tell "datatype content = "
tell (mkname (head ins))
forM_ (tail ins) (\f -> tell $ printf " | %s" (mkname f))
writeFile (replaceExtensions tgt_in "urs") $ execWriter $ do
line datatype
line "val binary : content -> transaction blob"
line "val text : content -> transaction string"
line "val blobpage : content -> transaction page"
line "val urls : list url"
writeFile (replaceExtensions tgt_in "ur") $ execWriter $ do
line datatype
line $ "fun binary c = case c of"
line $ printf " %s => %s.binary ()" (mkname (head ins)) (mkname (head ins))
forM_ (tail ins) (\f -> line $
printf " | %s => %s.binary ()" (mkname f) (mkname f))
line $ "fun blobpage c = case c of"
line $ printf " %s => %s.blobpage ()" (mkname (head ins)) (mkname (head ins))
forM_ (tail ins) (\f -> line $
printf " | %s => %s.blobpage ()" (mkname f) (mkname f))
line $ "fun text c = case c of"
line $ printf " %s => %s.text ()" (mkname (head ins)) (mkname (head ins))
forM_ (tail ins) (\f -> line $
printf " | %s => %s.text ()" (mkname f) (mkname f))
line $ "val urls ="
forM_ ins (\f -> line $
printf " %s.geturl :: " (mkname f))
line $ " []"
-- Build the Makefile
setCurrentDirectory tgtdir
writeFile ((takeBaseName tgturp) .= ".mk") =<< (mdo
let file x = C3.file' tgtdir tgtdir x
let cc = extvar "CC"
let ld = extvar "LD"
let incl = extvar "UR_INCLUDE_DIR"
let tgt_in = file (takeBaseName tgturp .= ".urp.in")
let tgt = file (takeBaseName tgturp .= ".urp")
urp_in <- C3.ruleM tgt_in $ do
flip urdeps tgt_in (
defaultConfig {
urObjRule = \f -> rule f $ do
case isInfixOf "data" (C3.takeExtensions f) of
True -> do
let src = C3.fromFilePath . (++"_blob") . dropExtensions . C3.toFilePath $ f
C3.shell [cmd| $(ld) -r -b binary -o $f $(src :: File) |]
False -> do
let src = C3.fromFilePath . flip replaceExtensions "c" . C3.toFilePath $ f
C3.shell [cmd| $(cc) -c -I $incl -o $f $(src :: File) |]
})
urp <- C3.ruleM tgt $ do
C3.shell [cmd|cp $(urp_in) $(urp) |]
C3.shell [cmd|echo $urp|]
runMake $ do
C3.place (phony "urp" (depend urp))
)
hPutStrLn stderr "Done"
where
line s = tell (s++"\n")
process = process' Nothing
process' wd args = do
(_,hout,herr,ph) <- runInteractiveProcess (head args) (tail args) wd Nothing
code <- waitForProcess ph
when (code /= ExitSuccess) $ do
hGetContents hout >>= hPutStrLn stderr
hGetContents herr >>= hPutStrLn stderr
fail $ printf "process %s failed to complete with %s" (show args) (show code)
return ()
mkname f = upper1 . notnum . map under . takeFileName $ f where
under c | c`elem`"_-. /" = '_'
| otherwise = c
upper1 [] = []
upper1 (x:xs) = (toUpper x) : xs
notnum n@(x:xs) | isDigit x = "f" ++ n
| otherwise = n