1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE OverloadedStrings #-}
4module Obelisk.Command.Project
7 , findProjectObeliskCommand
11 , inImpureProjectShell
19import Control.Monad.IO.Class (liftIO)
20import Control.Monad.State
22import Data.Function (on)
23import qualified Data.Text as T
24import System.Directory
27import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
28import System.Posix.Files
29import System.Process (CreateProcess, cwd, proc, waitForProcess)
31import GitHub.Data.GitData (Branch)
32import GitHub.Data.Name (Name)
34import Obelisk.App (MonadObelisk)
36import Obelisk.Command.Thunk
37import Obelisk.Command.Nix (withNixRemoteCheck)
38--TODO: Make this module resilient to random exceptions
40--TODO: Don't hardcode this
41-- | Source for the Obelisk project
42obeliskSource :: ThunkSource
43obeliskSource = obeliskSourceWithBranch "master"
45-- | Source for obelisk developer targeting a specific obelisk branch
46obeliskSourceWithBranch :: Name Branch -> ThunkSource
47obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
48 { _gitHubSource_owner = "obsidiansystems"
49 , _gitHubSource_repo = "obelisk"
50 , _gitHubSource_branch = Just branch
55 | InitSource_Branch (Name Branch)
56 | InitSource_Symlink FilePath
59-- | Path to obelisk directory in given path
60toObeliskDir :: FilePath -> FilePath
61toObeliskDir p = p </> ".obelisk"
63-- | Path to impl file in given path
64toImplDir :: FilePath -> FilePath
65toImplDir p = toObeliskDir p </> "impl"
67-- | Create a new project rooted in the current directory
68initProject :: MonadObelisk m => InitSource -> Bool -> m ()
69initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
70 let implDir = toImplDir tmpDir
71 obDir = toObeliskDir tmpDir
72 liftIO (listDirectory ".") >>= \case
74 _ | force -> putLog Warning "Initializing in non-empty directory"
75 | otherwise -> failWith "ob init requires an empty directory. Use the flag --force to init anyway, potentially overwriting files."
76 skeleton <- withSpinner "Setting up obelisk" $ do
77 liftIO $ createDirectory obDir
79 InitSource_Default -> createThunkWithLatest implDir obeliskSource
80 InitSource_Branch branch -> createThunkWithLatest implDir $ obeliskSourceWithBranch branch
81 InitSource_Symlink path -> do
82 let symlinkPath = if isAbsolute path
85 liftIO $ createSymbolicLink symlinkPath implDir
86 _ <- nixBuildAttrWithCache implDir "command"
87 --TODO: We should probably handoff to the impl here
88 skel <- nixBuildAttrWithCache implDir "skeleton" --TODO: I don't think there's actually any reason to cache this
90 callProcessAndLogOutput (Notice, Error) $
99 withSpinner "Copying project skeleton" $ do
100 callProcessAndLogOutput (Notice, Error) $
103 , "--no-preserve=mode"
109 let configDir = "config"
110 createDirectoryIfMissing False configDir
111 mapM_ (createDirectoryIfMissing False . (configDir </>)) ["backend", "common", "frontend"]
113--TODO: Allow the user to ignore our security concerns
114-- | Find the Obelisk implementation for the project at the given path
115findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
116findProjectObeliskCommand target = do
117 myUid <- liftIO getRealUserID
118 targetStat <- liftIO $ getFileStatus target
119 (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
120 Nothing -> pure Nothing
121 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
123 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
126 walkToImplDir projectRoot myUid -- For security check
127 return $ Just projectRoot
128 case (result, insecurePaths) of
129 (Just projDir, []) -> do
130 obeliskCommandPkg <- nixBuildAttrWithCache (toImplDir projDir) "command"
131 return $ Just $ obeliskCommandPkg </> "bin" </> "ob"
132 (Nothing, _) -> return Nothing
133 (Just projDir, _) -> do
134 putLog Error $ T.unlines
135 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
136 , T.unlines $ fmap (T.pack . normalise) insecurePaths
137 , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
141-- | Get the FilePath to the containing project directory, if there is one
142findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
143findProjectRoot target = do
144 myUid <- liftIO getRealUserID
145 targetStat <- liftIO $ getFileStatus target
146 (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
149withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m ()) -> m ()
150withProjectRoot target f = findProjectRoot target >>= \case
151 Nothing -> failWith "Must be used inside of an Obelisk project"
154-- | Walk from the current directory to the containing project's root directory,
155-- if there is one, accumulating potentially insecure directories that were
156-- traversed in the process. Return the project root directory, if found.
158 :: (MonadState [FilePath] m, MonadIO m)
159 => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
160walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
161 -- It's not a directory, so it can't be a project
163 let dir = takeDirectory this
164 dirStat <- liftIO $ getFileStatus dir
165 walkToProjectRoot dir dirStat myUid
167 when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
168 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
169 True -> return $ Just this
171 let next = this </> ".." -- Use ".." instead of chopping off path segments, so that if the current directory is moved during the traversal, the traversal stays consistent
172 nextStat <- liftIO $ getFileStatus next
173 let fileIdentity fs = (deviceID fs, fileID fs)
174 isSameFileAs = (==) `on` fileIdentity
175 if thisStat `isSameFileAs` nextStat
176 then return Nothing -- Found a cycle; probably hit root directory
177 else walkToProjectRoot next nextStat myUid
179-- | Walk from the given project root directory to its Obelisk implementation
180-- directory, accumulating potentially insecure directories that were traversed
182walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
183walkToImplDir projectRoot myUid = do
184 let obDir = toObeliskDir projectRoot
185 obDirStat <- liftIO $ getFileStatus obDir
186 when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
187 let implThunk = obDir </> "impl"
188 implThunkStat <- liftIO $ getFileStatus implThunk
189 when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
191--TODO: Is there a better way to ask if anyone else can write things?
192--E.g. what about ACLs?
193-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
194isWritableOnlyBy :: FileStatus -> UserID -> Bool
195isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
197-- | Run a command in the given shell for the current project
198inProjectShell :: MonadObelisk m => String -> String -> m ()
199inProjectShell shellName command = withProjectRoot "." $ \root ->
200 projectShell root True shellName command
202inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
203inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
204 projectShell root False shellName command
206projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
207projectShell root isPure shellName command = withNixRemoteCheck $ do
208 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ proc "nix-shell" $
209 [ "--pure" | isPure ] <>
211 , "shells." <> shellName
214 void $ liftIO $ waitForProcess ph
216setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
217setCwd fp cfg = cfg { cwd = fp }
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list