diff --git a/elm.cabal b/elm.cabal index 956b2ddd..8e2221d7 100644 --- a/elm.cabal +++ b/elm.cabal @@ -233,6 +233,7 @@ Executable lamdera Lamdera.CLI.Check Lamdera.CLI.Deploy Lamdera.CLI.Reset + Lamdera.CLI.Update -- CLI Experimental Lamdera.CLI.Annotate Lamdera.CLI.Interpreter diff --git a/extra/Lamdera.hs b/extra/Lamdera.hs index e330e1bd..3f5e8858 100644 --- a/extra/Lamdera.hs +++ b/extra/Lamdera.hs @@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE BangPatterns #-} module Lamdera @@ -61,6 +60,7 @@ module Lamdera , textContains , textHasPrefix , stringContains + , stringHasPrefix , fileContains , formatHaskellValue , hindent @@ -530,13 +530,16 @@ onlyWith filepath io = do textContains :: Text -> Text -> Bool -textContains needle haystack = T.isInfixOf needle haystack +textContains = T.isInfixOf textHasPrefix :: Text -> Text -> Bool -textHasPrefix needle haystack = T.isPrefixOf needle haystack +textHasPrefix = T.isPrefixOf stringContains :: String -> String -> Bool -stringContains needle haystack = List.isInfixOf needle haystack +stringContains = List.isInfixOf + +stringHasPrefix :: String -> String -> Bool +stringHasPrefix = List.isPrefixOf fileContains :: FilePath -> Text -> IO Bool fileContains filename needle = do diff --git a/extra/Lamdera/CLI.hs b/extra/Lamdera/CLI.hs index a30ddbf4..dd777f78 100644 --- a/extra/Lamdera/CLI.hs +++ b/extra/Lamdera/CLI.hs @@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} -module Lamdera.CLI (live, login, check, deploy, reset, annotate, eval) where +module Lamdera.CLI (live, login, check, deploy, reset, update, annotate, eval) where import Text.Read (readMaybe) import qualified Text.PrettyPrint.ANSI.Leijen as P @@ -13,6 +13,7 @@ import qualified Lamdera.CLI.Login import qualified Lamdera.CLI.Check import qualified Lamdera.CLI.Deploy import qualified Lamdera.CLI.Reset +import qualified Lamdera.CLI.Update import qualified Lamdera.CLI.Annotate import qualified Lamdera.CLI.Interpreter @@ -128,6 +129,25 @@ reset = Terminal.Command "reset" (Common summary) details example noArgs noFlags Lamdera.CLI.Reset.run +update :: Terminal.Command +update = + let + summary = + "Update the Lamdera compiler to the latest version if out of date." + + details = + "The latest versions of the Lamdera compiler are available at https://dashboard.lamdera.app/docs/download" + + example = + reflow + "It will find the latest lamdera binary version, download it, and replace itself." + + updateFlags = + flags Lamdera.CLI.Update.Flags + |-- onOff "force" "Force update to the latest published version, regardless of what version is installed currently." + in + Terminal.Command "update" (Common summary) details example noArgs updateFlags Lamdera.CLI.Update.run + annotate :: Terminal.Command annotate = diff --git a/extra/Lamdera/CLI/Check.hs b/extra/Lamdera/CLI/Check.hs index 736af959..300c6bcd 100644 --- a/extra/Lamdera/CLI/Check.hs +++ b/extra/Lamdera/CLI/Check.hs @@ -511,6 +511,7 @@ getNextVersionInfo_ nextVersion prodVersion isHoistRebuild localTypesChangedFrom checkForLatestBinaryVersion inDebug = do + progressPointer "Checking version..." latestVersionText_ <- Lamdera.Update.fetchCurrentVersion case latestVersionText_ of Right latestVersionText -> do @@ -551,12 +552,11 @@ checkForLatestBinaryVersion inDebug = do debug $ "comparing remote:" <> show latestVersion <> " local:" <> show localVersion onlyWhen (latestVersionText /= "skip" && latestVersion > localVersion) $ do - progressPointer "Checking version..." progressDoc $ D.stack [ D.red $ D.reflow $ "NOTE: There is a new lamdera version, please upgrade before you deploy." , D.reflow $ "Current: " <> Lamdera.Version.short , D.reflow $ "New: " <> T.unpack latestVersionText - , D.reflow $ "You can download it here: " + , D.reflow $ "Run `lamdera upgade`, or download it here: " ] onlyWhen (latestVersion < localVersion) $ do diff --git a/extra/Lamdera/CLI/Update.hs b/extra/Lamdera/CLI/Update.hs new file mode 100644 index 00000000..a1d4418e --- /dev/null +++ b/extra/Lamdera/CLI/Update.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- lamdera update – to update the lamdera binary +-- lamdera ugprade namespace left for potential future command to mirror elm-json upgrade + +module Lamdera.CLI.Update where + +import Control.Exception (bracket_) +import Data.List (isPrefixOf) +import Network.HTTP.Client +import qualified Data.Text as T +import qualified System.Info +import qualified Text.Read +import System.Directory (renameFile, getPermissions, setPermissions, executable) +import System.Environment (getExecutablePath, getProgName) +import System.FilePath (()) +import System.IO.Temp (withSystemTempDirectory) +import System.Process (callProcess) + +import qualified Http +import qualified Reporting +import qualified Reporting.Doc as D + +import Lamdera +import Lamdera.Version (Version) +import qualified Lamdera.Http +import qualified Lamdera.Update +import qualified Lamdera.Version + + +newtype Flags = + Flags + { _force :: Bool + } + + +run :: () -> Flags -> IO () +run _ flags@(Flags force) = do + binaryPath <- getExecutablePath + + onlyWhen (isInNixStore binaryPath) $ error $ + "Oops! Looks like lamdera is installed via Nix (" <> binaryPath <> ").\n" <> + "Skipping self-upgrade, please bump your Nix derivation manually." + + onlyWhen (isRunningInGHCi binaryPath) $ error $ + "Oops! Can't self-update in GHCi (detected by current path " <> binaryPath <> ")" + + latest <- Lamdera.Update.getLatestVersion + + case latest of + Left err -> do + atomicPutStrLn $ "Error fetching latest version: " <> show err + + Right latestVersion -> do + if not (Lamdera.Update.isLatest latestVersion) || force + then do + updateApproved <- do + Reporting.ask $ D.stack [ D.reflow $ + "Replace " <> binaryPath <> " with version " <> Lamdera.Version.rawToString latestVersion <> "? [Y/n]: " + ] + + onlyWhen updateApproved $ do + atomicPutStrLn $ "Downloading Lamdera from " <> downloadUrl latestVersion + withSystemTempDirectory "lamdera-upgrade" $ \tempDir -> do + let tempFilePath = tempDir "lamdera-new" + download <- Lamdera.Http.downloadToFile (downloadUrl latestVersion) tempFilePath + case download of + Left err -> do + atomicPutStrLn $ "Error downloading latest version: " <> show err + Right () -> do + overwriteBinary tempFilePath binaryPath + execNewBinary binaryPath + + else + atomicPutStrLn $ "No updates available, version " <> Lamdera.Version.rawToString latestVersion <> " is the latest." + + +downloadUrl :: Version -> String +downloadUrl version = + let + base = "https://static.lamdera.com/bin/lamdera" + + architecture = + case System.Info.arch of + "aarch64" -> "arm64" + "x86_64" -> "x86_64" + _ -> error "Oops! Unsupported architecture: " <> System.Info.arch <> ", please report this." + in + case ostype of + MacOS -> do + base <> "-" <> Lamdera.Version.rawToString version <> "-" <> "macos" <> "-" <> architecture + + Linux -> do + base <> "-" <> Lamdera.Version.rawToString version <> "-" <> "linux" <> "-" <> architecture + + Windows -> do + error "Oops, I can't auto-update on Windows, please download manually from https://dashboard.lamdera.app/docs/download" + + UnknownOS name -> do + error $ "Oops, I couldn't figure out the OS to upgrade for. Please report unknown OSTYPE: " <> show name + + +overwriteBinary :: FilePath -> FilePath -> IO () +overwriteBinary tempFilePath destFilePath = do + renameFile tempFilePath destFilePath + permissions <- getPermissions destFilePath + setPermissions destFilePath (permissions { executable = True }) + + +execNewBinary :: FilePath -> IO () +execNewBinary binaryPath = do + callProcess binaryPath [] + + +isInNixStore :: FilePath -> Bool +isInNixStore path = path & stringHasPrefix "/nix/store" + + +isRunningInGHCi :: String -> Bool +isRunningInGHCi binaryPath = + binaryPath & stringContains "/ghc/" diff --git a/extra/Lamdera/Http.hs b/extra/Lamdera/Http.hs index db2b8199..1064a126 100644 --- a/extra/Lamdera/Http.hs +++ b/extra/Lamdera/Http.hs @@ -1,22 +1,22 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE StandaloneDeriving #-} module Lamdera.Http where {- HTTP helpers and wrapper -} +import qualified Data.ByteString.Char8 as BS +import qualified Network.HTTP.Client as HTTP +import qualified Network.HTTP.Types.Header as Http + +import qualified Http import qualified Json.Decode as D import qualified Json.Encode as E -import qualified Network.HTTP.Types.Header as Http import Reporting.Exit -import qualified Http -import qualified Data.ByteString.Char8 as BS -import qualified Network.HTTP.Client as HTTP import Lamdera -import qualified Lamdera.Version import Lamdera.Progress +import qualified Lamdera.Version import StandaloneInstances @@ -26,15 +26,21 @@ data WithErrorField a deriving (Show) -jsonHeaders :: [Http.Header] -jsonHeaders = +defaultHeaders :: [Http.Header] +defaultHeaders = [ ( Http.hUserAgent, "lamdera-" <> BS.pack Lamdera.Version.short ) - , ( Http.hContentType, "application/json" ) - , ( Http.hAccept, "application/json" ) , ( Http.hAcceptEncoding, "gzip") ] +jsonHeaders :: [Http.Header] +jsonHeaders = + defaultHeaders ++ + [ ( Http.hContentType, "application/json" ) + , ( Http.hAccept, "application/json" ) + ] + + normalJson :: (Show a) => String -> String -> D.Decoder () a -> IO (Either Error a) normalJson debugIdentifier url decoder = do manager <- Http.getManager @@ -69,10 +75,18 @@ normalRpcJson debugIdentifier body url decoder = do return $ Left (JsonError url problem) +downloadToFile :: String -> FilePath -> IO (Either Error ()) +downloadToFile url path = do + manager <- Http.getManager + Http.get manager url [] HttpError $ \body -> do + BS.writeFile path body + pure $ Right () + + printHttpError :: Error -> String -> IO () printHttpError error reason = case error of - JsonError string dError -> putStrLn $ show error + JsonError string dError -> atomicPutStrLn $ show error HttpError httpError -> throw $ toHttpErrorReport "HTTP PROBLEM" httpError reason diff --git a/extra/Lamdera/Update.hs b/extra/Lamdera/Update.hs index ea89e53d..60e010f2 100644 --- a/extra/Lamdera/Update.hs +++ b/extra/Lamdera/Update.hs @@ -8,9 +8,13 @@ module Lamdera.Update where import Data.Text (Text) import qualified Data.Text as T import qualified Json.Decode as D -import qualified Lamdera.Http +import qualified Text.Read + import qualified Json.String +import Lamdera +import qualified Lamdera.Http +import qualified Lamdera.Version fetchCurrentVersion :: IO (Either Lamdera.Http.Error Text) fetchCurrentVersion = do @@ -22,3 +26,44 @@ fetchCurrentVersion = do D.string fmap (T.pack . Json.String.toChars) <$> Lamdera.Http.normalJson "fetchCurrentVersion" endpoint decoder + +getLatestVersion :: IO (Either String (Int, Int, Int)) +getLatestVersion = do + latestVersionText_ <- fetchCurrentVersion + case latestVersionText_ of + Right latestVersionText -> do + let + toIntCertain :: Text -> Int + toIntCertain t = + t & T.unpack + & Text.Read.readMaybe + & withDefault 0 + + latestVersion = + latestVersionText + & T.splitOn "-" + & (\parts -> + case parts of + ev:lv:_ -> + lv + & T.splitOn "." + & fmap toIntCertain + & (\parts_ -> + case parts_ of + [v1, v2, v3] -> + Right ( v1, v2, v3 ) + + _ -> + Left "Got an invalid lamdera version format" + ) + + _ -> Left "Got an invalid full version format" + ) + pure latestVersion + Left err -> + pure $ Left $ show err + + +isLatest :: Lamdera.Version.Version -> Bool +isLatest version = + version >= Lamdera.Version.raw diff --git a/extra/Lamdera/Version.hs b/extra/Lamdera/Version.hs index b60fcc59..aa38dedb 100644 --- a/extra/Lamdera/Version.hs +++ b/extra/Lamdera/Version.hs @@ -8,6 +8,8 @@ import qualified Ext.Common import qualified Elm.Version as V +type Version = (Int, Int, Int) + raw :: (Int, Int, Int) raw = (1,2,2) diff --git a/terminal/src/Main.hs b/terminal/src/Main.hs index 714bfa2c..c5f48c04 100644 --- a/terminal/src/Main.hs +++ b/terminal/src/Main.hs @@ -45,6 +45,7 @@ main = , make , repl , Lamdera.CLI.reset + , Lamdera.CLI.update , Lamdera.CLI.annotate , Lamdera.CLI.eval -- , reactor