diff --git a/parser-typechecker/src/Unison/Runtime/Exception.hs b/parser-typechecker/src/Unison/Runtime/Exception.hs index 11f5fdbc1c..dff4a627b7 100644 --- a/parser-typechecker/src/Unison/Runtime/Exception.hs +++ b/parser-typechecker/src/Unison/Runtime/Exception.hs @@ -18,5 +18,8 @@ instance Exception RuntimeExn die :: (HasCallStack) => String -> IO a die = throwIO . PE callStack . P.lit . fromString +dieP :: HasCallStack => P.Pretty P.ColorText -> IO a +dieP = throwIO . PE callStack + exn :: (HasCallStack) => String -> a exn = throw . PE callStack . P.lit . fromString diff --git a/parser-typechecker/src/Unison/Runtime/Interface.hs b/parser-typechecker/src/Unison/Runtime/Interface.hs index 72513a47bf..056c8a3b48 100644 --- a/parser-typechecker/src/Unison/Runtime/Interface.hs +++ b/parser-typechecker/src/Unison/Runtime/Interface.hs @@ -21,6 +21,7 @@ module Unison.Runtime.Interface where import Control.Concurrent.STM as STM +import Control.Exception (throwIO) import Control.Monad import Data.Binary.Get (runGetOrFail) -- import Data.Bits (shiftL) @@ -44,17 +45,21 @@ import Data.Set as Set ) import Data.Set qualified as Set import Data.Text (isPrefixOf, unpack) +import GHC.Stack (callStack) import System.Directory ( XdgDirectory(XdgCache), createDirectoryIfMissing, getXdgDirectory ) +import System.Exit (ExitCode(..)) import System.FilePath ((<.>), ()) import System.Process ( CreateProcess (..), StdStream (..), callProcess, proc, + readCreateProcessWithExitCode, + shell, waitForProcess, withCreateProcess, ) @@ -440,18 +445,19 @@ decompileCtx crs ctx = decompile ib $ backReferenceTm crs fr ir dt dt = decompTm ctx nativeEval :: + FilePath -> IORef EvalCtx -> CodeLookup Symbol IO () -> PrettyPrintEnv -> Term Symbol -> IO (Either Error ([Error], Term Symbol)) -nativeEval ctxVar cl ppe tm = catchInternalErrors $ do +nativeEval execDir ctxVar cl ppe tm = catchInternalErrors $ do ctx <- readIORef ctxVar (tyrs, tmrs) <- collectDeps cl tm (ctx, codes) <- loadDeps cl ppe ctx tyrs tmrs (ctx, tcodes, base) <- prepareEvaluation ppe tm ctx writeIORef ctxVar ctx - nativeEvalInContext ppe ctx (codes ++ tcodes) base + nativeEvalInContext execDir ppe ctx (codes ++ tcodes) base interpEval :: ActiveThreads -> @@ -472,20 +478,82 @@ interpEval activeThreads cleanupThreads ctxVar cl ppe tm = evalInContext ppe ctx activeThreads initw `UnliftIO.finally` cleanupThreads +ensureExists :: HasCallStack => CreateProcess -> Pretty ColorText -> IO () +ensureExists cmd err = + ccall >>= \case + True -> pure () + False -> dieP err + where + call = readCreateProcessWithExitCode cmd "" >>= \case + (ExitSuccess, _, _) -> pure True + (ExitFailure _, _, _) -> pure False + ccall = call `UnliftIO.catch` \(_ :: IOException) -> pure False + +ensureRuntimeExists :: HasCallStack => FilePath -> IO () +ensureRuntimeExists execDir = ensureExists cmd (runtimeErrMsg execDir) + where + cmd = proc (ucrFile execDir) ["--help"] + +ensureRacoExists :: HasCallStack => IO () +ensureRacoExists = ensureExists (shell "raco help") racoErrMsg + +runtimeErrMsg :: String -> Pretty ColorText +runtimeErrMsg execDir = + P.lines + [ P.wrap + "I can't seem to call `unison-runtime`. I was looking for\ + \ it at:", + "", + P.indentN + 2 + (fromString $ ucrFile execDir), + "", + "See", + "", + P.indentN + 2 + "TODO", + "", + P.wrap + "for detailed instructions on how to install unison with this\ + \ feature available.", + "", + P.wrap + "If you have the executable installed somewhere else, you can\ + \ use the `--runtime-path` command line argument to specify\ + \ where it is." + ] + +racoErrMsg :: Pretty ColorText +racoErrMsg = + P.lines + [ P.wrap + "I can't seem to call `raco`. Please ensure Racket \ + \is installed.", + "", + "See", + "", + P.indentN + 2 + "https://download.racket-lang.org/", + "", + "for how to install Racket manually." + ] + nativeCompile :: - Text -> + FilePath -> IORef EvalCtx -> CodeLookup Symbol IO () -> PrettyPrintEnv -> Reference -> FilePath -> IO (Maybe Error) -nativeCompile _version ctxVar cl ppe base path = tryM $ do +nativeCompile execDir ctxVar cl ppe base path = tryM $ do ctx <- readIORef ctxVar (tyrs, tmrs) <- collectRefDeps cl base (ctx, codes) <- loadDeps cl ppe ctx tyrs tmrs Just ibase <- pure $ baseToIntermed ctx base - nativeCompileCodes codes ibase path + nativeCompileCodes execDir codes ibase path interpCompile :: Text -> @@ -655,9 +723,12 @@ backReferenceTm ws frs irs dcm c i = do bs <- Map.lookup r dcm Map.lookup i bs -ucrProc :: [String] -> CreateProcess -ucrProc args = - (proc "native-compiler/bin/unison-runtime" args) +ucrFile :: FilePath -> FilePath +ucrFile execDir = execDir "unison-runtime" + +ucrProc :: FilePath -> [String] -> CreateProcess +ucrProc execDir args = + (proc (ucrFile execDir) args) { std_in = CreatePipe, std_out = Inherit, std_err = Inherit @@ -675,12 +746,14 @@ ucrProc args = -- taken over the input. This could probably be without a side -- channel, but a side channel is probably better. nativeEvalInContext :: + FilePath -> PrettyPrintEnv -> EvalCtx -> [(Reference, SuperGroup Symbol)] -> Reference -> IO (Either Error ([Error], Term Symbol)) -nativeEvalInContext _ ctx codes base = do +nativeEvalInContext execDir _ ctx codes base = do + ensureRuntimeExists execDir let cc = ccache ctx crs <- readTVarIO $ combRefs cc let bytes = serializeValue . compileValue base $ codes @@ -704,19 +777,19 @@ nativeEvalInContext _ ctx codes base = do -- decodeResult . deserializeValue =<< BS.hGetContents pout callout _ _ _ _ = pure . Left $ "withCreateProcess didn't provide handles" - ucrError (_ :: IOException) = - die - "I had trouble calling the unison runtime exectuable.\n\n\ - \Please check that the `unison-runtime` executable is\ - \properly installed." - withCreateProcess (ucrProc []) callout `UnliftIO.catch` ucrError + ucrError (_ :: IOException) = pure $ Left (runtimeErrMsg execDir) + withCreateProcess (ucrProc execDir []) callout + `UnliftIO.catch` ucrError nativeCompileCodes :: + FilePath -> [(Reference, SuperGroup Symbol)] -> Reference -> FilePath -> IO () -nativeCompileCodes codes base path = do +nativeCompileCodes execDir codes base path = do + ensureRuntimeExists execDir + ensureRacoExists genDir <- getXdgDirectory XdgCache "unisonlanguage/racket-tmp" createDirectoryIfMissing True genDir let bytes = serializeValue . compileValue base $ codes @@ -729,15 +802,9 @@ nativeCompileCodes codes base path = do pure () callout _ _ _ _ = fail "withCreateProcess didn't provide handles" ucrError (_ :: IOException) = - die - "I had trouble calling the unison runtime exectuable.\n\n\ - \Please check that the `unison-runtime` executable is\ - \properly installed." - racoError (_ :: IOException) = - die - "I had trouble calling the `raco` executable.\n\n\ - \Please verify that you have racket installed." - withCreateProcess (ucrProc ["-G", srcPath]) callout + throwIO $ PE callStack (runtimeErrMsg execDir) + racoError (_ :: IOException) = throwIO $ PE callStack racoErrMsg + withCreateProcess (ucrProc execDir ["-G", srcPath]) callout `UnliftIO.catch` ucrError callProcess "raco" ["exe", "-o", path, srcPath] `UnliftIO.catch` racoError @@ -900,7 +967,11 @@ icon = "💔💥" catchInternalErrors :: IO (Either Error a) -> IO (Either Error a) -catchInternalErrors sub = sub `UnliftIO.catch` \(CE _ e) -> pure $ Left e +catchInternalErrors sub = sub `UnliftIO.catch` hCE `UnliftIO.catch` hRE + where + hCE (CE _ e) = pure $ Left e + hRE (PE _ e) = pure $ Left e + hRE (BU _ _ _) = pure $ Left "impossible" decodeStandalone :: BL.ByteString -> @@ -945,14 +1016,14 @@ startRuntime sandboxed runtimeHost version = do ioTestTypes = builtinIOTestTypes External } -startNativeRuntime :: Text -> IO (Runtime Symbol) -startNativeRuntime version = do +startNativeRuntime :: Text -> FilePath -> IO (Runtime Symbol) +startNativeRuntime _version execDir = do ctxVar <- newIORef =<< baseContext False pure $ Runtime { terminate = pure (), - evaluate = nativeEval ctxVar, - compileTo = nativeCompile version ctxVar, + evaluate = nativeEval execDir ctxVar, + compileTo = nativeCompile execDir ctxVar, mainType = builtinMain External, ioTestTypes = builtinIOTestTypes External } @@ -962,10 +1033,14 @@ withRuntime sandboxed runtimeHost version action = UnliftIO.bracket (liftIO $ startRuntime sandboxed runtimeHost version) (liftIO . terminate) action tryM :: IO () -> IO (Maybe Error) -tryM = fmap (either (Just . extract) (const Nothing)) . try +tryM = + flip UnliftIO.catch hRE . + flip UnliftIO.catch hCE . + fmap (const Nothing) where - extract (PE _ e) = e - extract (BU _ _ _) = "impossible" + hCE (CE _ e) = pure $ Just e + hRE (PE _ e) = pure $ Just e + hRE (BU _ _ _) = pure $ Just "impossible" runStandalone :: StoredCache -> Word64 -> IO (Either (Pretty ColorText) ()) runStandalone sc init = diff --git a/unison-cli/src/Unison/Codebase/TranscriptParser.hs b/unison-cli/src/Unison/Codebase/TranscriptParser.hs index f49bea960c..9fe6cae89a 100644 --- a/unison-cli/src/Unison/Codebase/TranscriptParser.hs +++ b/unison-cli/src/Unison/Codebase/TranscriptParser.hs @@ -196,11 +196,12 @@ withTranscriptRunner :: (UnliftIO.MonadUnliftIO m) => Verbosity -> UCMVersion -> + FilePath -> Maybe FilePath -> (TranscriptRunner -> m r) -> m r -withTranscriptRunner verbosity ucmVersion configFile action = do - withRuntimes \runtime sbRuntime nRuntime -> withConfig \config -> do +withTranscriptRunner verbosity ucmVersion nrtp configFile action = do + withRuntimes nrtp \runtime sbRuntime nRuntime -> withConfig \config -> do action \transcriptName transcriptSrc (codebaseDir, codebase) -> do Server.startServer (Backend.BackendEnv {Backend.useNamesIndex = False}) Server.defaultCodebaseServerOpts runtime codebase \baseUrl -> do let parsed = parse transcriptName transcriptSrc @@ -209,12 +210,12 @@ withTranscriptRunner verbosity ucmVersion configFile action = do pure $ join @(Either TranscriptError) result where withRuntimes :: - (Runtime.Runtime Symbol -> Runtime.Runtime Symbol -> Runtime.Runtime Symbol -> m a) -> m a - withRuntimes action = + FilePath -> (Runtime.Runtime Symbol -> Runtime.Runtime Symbol -> Runtime.Runtime Symbol -> m a) -> m a + withRuntimes nrtp action = RTI.withRuntime False RTI.Persistent ucmVersion \runtime -> do RTI.withRuntime True RTI.Persistent ucmVersion \sbRuntime -> do action runtime sbRuntime - =<< liftIO (RTI.startNativeRuntime ucmVersion) + =<< liftIO (RTI.startNativeRuntime ucmVersion nrtp) withConfig :: forall a. ((Maybe Config -> m a) -> m a) withConfig action = do case configFile of diff --git a/unison-cli/unison/ArgParse.hs b/unison-cli/unison/ArgParse.hs index 971e289ec5..9e0a596b15 100644 --- a/unison-cli/unison/ArgParse.hs +++ b/unison-cli/unison/ArgParse.hs @@ -117,7 +117,8 @@ data Command -- | Options shared by sufficiently many subcommands. data GlobalOptions = GlobalOptions { codebasePathOption :: Maybe CodebasePathOption, - exitOption :: ShouldExit + exitOption :: ShouldExit, + nativeRuntimePath :: Maybe FilePath } deriving (Show, Eq) @@ -259,11 +260,13 @@ globalOptionsParser = do -- ApplicativeDo codebasePathOption <- codebasePathParser <|> codebaseCreateParser exitOption <- exitParser + nativeRuntimePath <- nativeRuntimePathFlag pure GlobalOptions { codebasePathOption = codebasePathOption, - exitOption = exitOption + exitOption = exitOption, + nativeRuntimePath = nativeRuntimePath } codebasePathParser :: Parser (Maybe CodebasePathOption) @@ -446,6 +449,14 @@ readAbsolutePath = do <> show rel <> " was relative. Try adding a `.` prefix, e.g. `.path.to.project`" +nativeRuntimePathFlag :: Parser (Maybe FilePath) +nativeRuntimePathFlag = + optional . strOption $ + long "runtime-path" + <> metavar "DIR" + <> help "Path to native runtime files" + <> noGlobal + readPath' :: ReadM Path.Path' readPath' = do strPath <- OptParse.str diff --git a/unison-cli/unison/Main.hs b/unison-cli/unison/Main.hs index 42664ec3f8..98fb5cec71 100644 --- a/unison-cli/unison/Main.hs +++ b/unison-cli/unison/Main.hs @@ -15,7 +15,12 @@ where import ArgParse ( CodebasePathOption (..), Command (Init, Launch, PrintVersion, Run, Transcript), - GlobalOptions (GlobalOptions, codebasePathOption, exitOption), + GlobalOptions + ( GlobalOptions, + codebasePathOption, + exitOption, + nativeRuntimePath + ), IsHeadless (Headless, WithCLI), RunSource (..), ShouldExit (DoNotExit, Exit), @@ -41,7 +46,7 @@ import Ki qualified import Network.HTTP.Client qualified as HTTP import Network.HTTP.Client.TLS qualified as HTTP import Stats (recordRtsStats) -import System.Directory (canonicalizePath, getCurrentDirectory, removeDirectoryRecursive) +import System.Directory (canonicalizePath, getCurrentDirectory, removeDirectoryRecursive, getXdgDirectory, XdgDirectory(..)) import System.Environment (getProgName, withArgs) import System.Exit qualified as Exit import System.FilePath qualified as FP @@ -87,6 +92,11 @@ import Version qualified type Runtimes = (RTI.Runtime Symbol, RTI.Runtime Symbol, RTI.Runtime Symbol) +fixNativeRuntimePath :: Maybe FilePath -> IO FilePath +fixNativeRuntimePath = maybe dflt pure + where + dflt = getXdgDirectory XdgData ("unisonlanguage" FP. "libexec") + main :: IO () main = do -- Replace the default exception handler with one complains loudly, because we shouldn't have any uncaught exceptions. @@ -120,6 +130,7 @@ main = do progName <- getProgName -- hSetBuffering stdout NoBuffering -- cool (renderUsageInfo, globalOptions, command) <- parseCLIArgs progName (Text.unpack Version.gitDescribeWithDate) + nrtp <- fixNativeRuntimePath (nativeRuntimePath globalOptions) let GlobalOptions {codebasePathOption = mCodePathOption, exitOption = exitOption} = globalOptions withConfig mCodePathOption \config -> do currentDir <- getCurrentDirectory @@ -152,7 +163,7 @@ main = do Left _ -> exitError "I couldn't find that file or it is for some reason unreadable." Right contents -> do getCodebaseOrExit mCodePathOption (SC.MigrateAutomatically SC.Backup SC.Vacuum) \(initRes, _, theCodebase) -> do - withRuntimes RTI.OneOff \(rt, sbrt, nrt) -> do + withRuntimes nrtp RTI.OneOff \(rt, sbrt, nrt) -> do let fileEvent = Input.UnisonFileChanged (Text.pack file) contents let noOpRootNotifier _ = pure () let noOpPathNotifier _ = pure () @@ -178,7 +189,7 @@ main = do Left _ -> exitError "I had trouble reading this input." Right contents -> do getCodebaseOrExit mCodePathOption (SC.MigrateAutomatically SC.Backup SC.Vacuum) \(initRes, _, theCodebase) -> do - withRuntimes RTI.OneOff \(rt, sbrt, nrt) -> do + withRuntimes nrtp RTI.OneOff \(rt, sbrt, nrt) -> do let fileEvent = Input.UnisonFileChanged (Text.pack "") contents let noOpRootNotifier _ = pure () let noOpPathNotifier _ = pure () @@ -263,13 +274,13 @@ main = do \that matches your version of Unison." ] Transcript shouldFork shouldSaveCodebase mrtsStatsFp transcriptFiles -> do - let action = runTranscripts Verbosity.Verbose renderUsageInfo shouldFork shouldSaveCodebase mCodePathOption transcriptFiles + let action = runTranscripts Verbosity.Verbose renderUsageInfo shouldFork shouldSaveCodebase mCodePathOption nrtp transcriptFiles case mrtsStatsFp of Nothing -> action Just fp -> recordRtsStats fp action Launch isHeadless codebaseServerOpts mayStartingPath shouldWatchFiles -> do getCodebaseOrExit mCodePathOption (SC.MigrateAfterPrompt SC.Backup SC.Vacuum) \(initRes, _, theCodebase) -> do - withRuntimes RTI.Persistent \(runtime, sbRuntime, nRuntime) -> do + withRuntimes nrtp RTI.Persistent \(runtime, sbRuntime, nRuntime) -> do startingPath <- case isHeadless of WithCLI -> do -- If the user didn't provide a starting path on the command line, put them in the most recent @@ -334,12 +345,12 @@ main = do Exit -> do Exit.exitSuccess where -- (runtime, sandboxed runtime) - withRuntimes :: RTI.RuntimeHost -> (Runtimes -> IO a) -> IO a - withRuntimes mode action = + withRuntimes :: FilePath -> RTI.RuntimeHost -> (Runtimes -> IO a) -> IO a + withRuntimes nrtp mode action = RTI.withRuntime False mode Version.gitDescribeWithDate \runtime -> do RTI.withRuntime True mode Version.gitDescribeWithDate \sbRuntime -> action . (runtime,sbRuntime,) - =<< RTI.startNativeRuntime Version.gitDescribeWithDate + =<< RTI.startNativeRuntime Version.gitDescribeWithDate nrtp withConfig :: Maybe CodebasePathOption -> (Config -> IO a) -> IO a withConfig mCodePathOption action = do UnliftIO.bracket @@ -391,14 +402,15 @@ runTranscripts' :: String -> Maybe FilePath -> FilePath -> + FilePath -> NonEmpty MarkdownFile -> IO Bool -runTranscripts' progName mcodepath transcriptDir markdownFiles = do +runTranscripts' progName mcodepath nativeRtp transcriptDir markdownFiles = do currentDir <- getCurrentDirectory configFilePath <- getConfigFilePath mcodepath -- We don't need to create a codebase through `getCodebaseOrExit` as we've already done so previously. and <$> getCodebaseOrExit (Just (DontCreateCodebaseWhenMissing transcriptDir)) (SC.MigrateAutomatically SC.Backup SC.Vacuum) \(_, codebasePath, theCodebase) -> do - TR.withTranscriptRunner Verbosity.Verbose Version.gitDescribeWithDate (Just configFilePath) $ \runTranscript -> do + TR.withTranscriptRunner Verbosity.Verbose Version.gitDescribeWithDate nativeRtp (Just configFilePath) $ \runTranscript -> do for markdownFiles $ \(MarkdownFile fileName) -> do transcriptSrc <- readUtf8 fileName result <- runTranscript fileName transcriptSrc (codebasePath, theCodebase) @@ -446,9 +458,10 @@ runTranscripts :: ShouldForkCodebase -> ShouldSaveCodebase -> Maybe CodebasePathOption -> + FilePath -> NonEmpty String -> IO () -runTranscripts verbosity renderUsageInfo shouldFork shouldSaveTempCodebase mCodePathOption args = do +runTranscripts verbosity renderUsageInfo shouldFork shouldSaveTempCodebase mCodePathOption nativeRtp args = do markdownFiles <- case traverse (first (pure @[]) . markdownFile) args of Failure invalidArgs -> do PT.putPrettyLn $ @@ -466,7 +479,7 @@ runTranscripts verbosity renderUsageInfo shouldFork shouldSaveTempCodebase mCode progName <- getProgName transcriptDir <- prepareTranscriptDir verbosity shouldFork mCodePathOption shouldSaveTempCodebase completed <- - runTranscripts' progName (Just transcriptDir) transcriptDir markdownFiles + runTranscripts' progName (Just transcriptDir) nativeRtp transcriptDir markdownFiles case shouldSaveTempCodebase of DontSaveCodebase -> removeDirectoryRecursive transcriptDir SaveCodebase _ ->