Skip to content

Commit da95489

Browse files
committed
simple store path root, remote store rework
Closes #15, #16, #21, #22, #62.
1 parent 46cf3fc commit da95489

File tree

20 files changed

+1279
-229
lines changed

20 files changed

+1279
-229
lines changed

hnix-store-core/hnix-store-core.cabal

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cabal-version: >=1.10
1818

1919
library
2020
exposed-modules: System.Nix.Base32
21+
, System.Nix.Build
2122
, System.Nix.Hash
2223
, System.Nix.Internal.Base32
2324
, System.Nix.Internal.Hash
@@ -28,8 +29,8 @@ library
2829
, System.Nix.Signature
2930
, System.Nix.StorePath
3031
, System.Nix.StorePathMetadata
31-
, System.Nix.Util
3232
build-depends: base >=4.10 && <5
33+
, attoparsec
3334
, base16-bytestring
3435
, bytestring
3536
, binary
@@ -42,8 +43,6 @@ library
4243
, filepath
4344
, hashable
4445
, mtl
45-
, regex-base
46-
, regex-tdfa >= 1.3.1.0
4746
, saltine
4847
, time
4948
, text
@@ -67,16 +66,19 @@ test-suite format-tests
6766
Arbitrary
6867
NarFormat
6968
Hash
69+
StorePath
7070
hs-source-dirs:
7171
tests
7272
build-depends:
7373
hnix-store-core
74+
, attoparsec
7475
, base
7576
, base16-bytestring
7677
, base64-bytestring
7778
, binary
7879
, bytestring
7980
, containers
81+
, filepath
8082
, directory
8183
, process
8284
, tasty
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
{-|
3+
Description : Build related types
4+
Maintainer : srk <[email protected]>
5+
|-}
6+
module System.Nix.Build (
7+
BuildMode(..)
8+
, BuildStatus(..)
9+
, BuildResult(..)
10+
, buildSuccess
11+
) where
12+
13+
import Data.Time (UTCTime)
14+
import Data.Text (Text)
15+
import Data.HashSet (HashSet)
16+
17+
-- keep the order of these Enums to match enums from reference implementations
18+
-- src/libstore/store-api.hh
19+
data BuildMode = Normal | Repair | Check
20+
deriving (Eq, Ord, Enum, Show)
21+
22+
data BuildStatus =
23+
Built
24+
| Substituted
25+
| AlreadyValid
26+
| PermanentFailure
27+
| InputRejected
28+
| OutputRejected
29+
| TransientFailure -- possibly transient
30+
| CachedFailure -- no longer used
31+
| TimedOut
32+
| MiscFailure
33+
| DependencyFailed
34+
| LogLimitExceeded
35+
| NotDeterministic
36+
deriving (Eq, Ord, Enum, Show)
37+
38+
39+
-- | Result of the build
40+
data BuildResult = BuildResult
41+
{ -- | build status, MiscFailure should be default
42+
status :: !BuildStatus
43+
, -- | possible build error message
44+
errorMessage :: !(Maybe Text)
45+
, -- | How many times this build was performed
46+
timesBuilt :: !Integer
47+
, -- | If timesBuilt > 1, whether some builds did not produce the same result
48+
isNonDeterministic :: !Bool
49+
, -- Start time of this build
50+
startTime :: !UTCTime
51+
, -- Stop time of this build
52+
stopTime :: !UTCTime
53+
} deriving (Eq, Ord, Show)
54+
55+
buildSuccess BuildResult{..} = status == Built || status == Substituted || status == AlreadyValid

hnix-store-core/src/System/Nix/Internal/StorePath.hs

Lines changed: 112 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,28 @@ import System.Nix.Hash
1616
( HashAlgorithm(Truncated, SHA256)
1717
, Digest
1818
, encodeBase32
19+
, decodeBase32
1920
, SomeNamedDigest
2021
)
21-
import Text.Regex.Base.RegexLike (makeRegex, matchTest)
22-
import Text.Regex.TDFA.Text (Regex)
22+
import System.Nix.Internal.Base32 (digits32)
23+
2324
import Data.Text (Text)
2425
import Data.Text.Encoding (encodeUtf8)
26+
import qualified Data.Text as T
2527
import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)
2628
import Data.ByteString (ByteString)
2729
import qualified Data.ByteString as BS
2830
import qualified Data.ByteString.Char8 as BC
31+
import qualified Data.Char
2932
import Data.Hashable (Hashable(..))
3033
import Data.HashSet (HashSet)
3134
import Data.Proxy (Proxy(..))
3235

36+
import Data.Attoparsec.Text.Lazy (Parser, (<?>))
37+
38+
import qualified Data.Attoparsec.Text.Lazy
39+
import qualified System.FilePath
40+
3341
-- | A path in a Nix store.
3442
--
3543
-- From the Nix thesis: A store path is the full path of a store
@@ -39,20 +47,25 @@ import Data.Proxy (Proxy(..))
3947
--
4048
-- See the 'StoreDir' haddocks for details on why we represent this at
4149
-- the type level.
42-
data StorePath (storeDir :: StoreDir) = StorePath
50+
data StorePath = StorePath
4351
{ -- | The 160-bit hash digest reflecting the "address" of the name.
4452
-- Currently, this is a truncated SHA256 hash.
4553
storePathHash :: !(Digest StorePathHashAlgo)
4654
, -- | The (typically human readable) name of the path. For packages
4755
-- this is typically the package name and version (e.g.
4856
-- hello-1.2.3).
4957
storePathName :: !StorePathName
58+
, -- | Root of the store
59+
storePathRoot :: !FilePath
5060
} deriving (Eq, Ord)
5161

52-
instance Hashable (StorePath storeDir) where
62+
instance Hashable StorePath where
5363
hashWithSalt s (StorePath {..}) =
5464
s `hashWithSalt` storePathHash `hashWithSalt` storePathName
5565

66+
instance Show StorePath where
67+
show p = BC.unpack $ storePathToRawFilePath p
68+
5669
-- | The name portion of a Nix path.
5770
--
5871
-- 'unStorePathName' must only contain a-zA-Z0-9+._?=-, can't start
@@ -67,7 +80,7 @@ newtype StorePathName = StorePathName
6780
type StorePathHashAlgo = 'Truncated 20 'SHA256
6881

6982
-- | A set of 'StorePath's.
70-
type StorePathSet storeDir = HashSet (StorePath storeDir)
83+
type StorePathSet = HashSet StorePath
7184

7285
-- | An address for a content-addressable store path, i.e. one whose
7386
-- store path hash is purely a function of its contents (as opposed to
@@ -99,103 +112,116 @@ data NarHashMode
99112
-- file if so desired.
100113
Recursive
101114

102-
-- | A type-level representation of the root directory of a Nix store.
103-
--
104-
-- The extra complexity of type indices requires justification.
105-
-- Fundamentally, this boils down to the fact that there is little
106-
-- meaningful sense in which 'StorePath's rooted at different
107-
-- directories are of the same type, i.e. there are few if any
108-
-- non-trivial non-contrived functions or data types that could
109-
-- equally well accept 'StorePath's from different stores. In current
110-
-- practice, any real application dealing with Nix stores (including,
111-
-- in particular, the Nix expression language) only operates over one
112-
-- store root and only cares about 'StorePath's belonging to that
113-
-- root. One could imagine a use case that cares about multiple store
114-
-- roots at once (e.g. the normal \/nix\/store along with some private
115-
-- store at \/root\/nix\/store to contain secrets), but in that case
116-
-- distinguishing 'StorePath's that belong to one store or the other
117-
-- is even /more/ critical: Most operations will only be correct over
118-
-- one of the stores or another, and it would be an error to mix and
119-
-- match (e.g. a 'StorePath' in one store could not legitimately refer
120-
-- to one in another).
121-
--
122-
-- As of @5886bc5996537fbf00d1fcfbb29595b8ccc9743e@, the C++ Nix
123-
-- codebase contains 30 separate places where we assert that a given
124-
-- store dir is, in fact, in the store we care about; those run-time
125-
-- assertions could be completely removed if we had stronger types
126-
-- there. Moreover, there are dozens of other cases where input coming
127-
-- from the user, from serializations, etc. is parsed and then
128-
-- required to be in the appropriate store; this case is the
129-
-- equivalent of an existentially quantified version of 'StorePath'
130-
-- and, notably, requiring at runtime that the index matches the
131-
-- ambient store directory we're working in. In every case where a
132-
-- path is treated as a store path, there is exactly one legitimate
133-
-- candidate for the store directory it belongs to.
134-
--
135-
-- It may be instructive to consider the example of "chroot stores".
136-
-- Since Nix 2.0, it has been possible to have a store actually live
137-
-- at one directory (say, $HOME\/nix\/store) with a different
138-
-- effective store directory (say, \/nix\/store). Nix can build into
139-
-- a chroot store by running the builds in a mount namespace where the
140-
-- store is at the effective store directory, can download from a
141-
-- binary cache containing paths for the effective store directory,
142-
-- and can run programs in the store that expect to be living at the
143-
-- effective store directory (via nix run). When viewed as store paths
144-
-- (rather than random files in the filesystem), paths in a chroot
145-
-- store have nothing in common with paths in a non-chroot store that
146-
-- lives in the same directory, and a lot in common with paths in a
147-
-- non-chroot store that lives in the effective store directory of the
148-
-- store in question. Store paths in stores with the same effective
149-
-- store directory share the same hashing scheme, can be copied
150-
-- between each other, etc. Store paths in stores with different
151-
-- effective store directories have no relationship to each other that
152-
-- they don't have to arbitrary other files.
153-
type StoreDir = Symbol
154-
155-
-- | Smart constructor for 'StorePathName' that ensures the underlying
156-
-- content invariant is met.
157-
makeStorePathName :: Text -> Maybe StorePathName
158-
makeStorePathName n = case matchTest storePathNameRegex n of
159-
True -> Just $ StorePathName n
160-
False -> Nothing
161-
162-
-- | Regular expression to match valid store path names.
163-
storePathNameRegex :: Regex
164-
storePathNameRegex = makeRegex r
165-
where
166-
r :: String
167-
r = "[a-zA-Z0-9\\+\\-\\_\\?\\=][a-zA-Z0-9\\+\\-\\.\\_\\?\\=]*"
115+
makeStorePathName :: Text -> Either String StorePathName
116+
makeStorePathName n = case validStorePathName n of
117+
True -> Right $ StorePathName n
118+
False -> Left $ reasonInvalid n
119+
120+
reasonInvalid :: Text -> String
121+
reasonInvalid n | n == "" = "Empty name"
122+
reasonInvalid n | (T.length n > 211) = "Path too long"
123+
reasonInvalid n | (T.head n == '.') = "Leading dot"
124+
reasonInvalid n | otherwise = "Invalid character"
125+
126+
validStorePathName :: Text -> Bool
127+
validStorePathName "" = False
128+
validStorePathName n = (T.length n <= 211)
129+
&& T.head n /= '.'
130+
&& T.all validStorePathNameChar n
131+
132+
validStorePathNameChar :: Char -> Bool
133+
validStorePathNameChar c = any ($ c) $
134+
[ Data.Char.isAsciiLower -- 'a'..'z'
135+
, Data.Char.isAsciiUpper -- 'A'..'Z'
136+
, Data.Char.isDigit
137+
] ++
138+
map (==) "+-._?="
168139

169140
-- | Copied from @RawFilePath@ in the @unix@ package, duplicated here
170141
-- to avoid the dependency.
171142
type RawFilePath = ByteString
172143

173144
-- | Render a 'StorePath' as a 'RawFilePath'.
174145
storePathToRawFilePath
175-
:: forall storeDir . (KnownStoreDir storeDir)
176-
=> StorePath storeDir
146+
:: StorePath
177147
-> RawFilePath
178-
storePathToRawFilePath (StorePath {..}) = BS.concat
148+
storePathToRawFilePath StorePath {..} = BS.concat
179149
[ root
180150
, "/"
181151
, hashPart
182152
, "-"
183153
, name
184154
]
185155
where
186-
root = storeDirVal @storeDir
156+
root = BC.pack storePathRoot
187157
hashPart = encodeUtf8 $ encodeBase32 storePathHash
188158
name = encodeUtf8 $ unStorePathName storePathName
189159

190-
-- | Get a value-level representation of a 'KnownStoreDir'
191-
storeDirVal :: forall storeDir . (KnownStoreDir storeDir)
192-
=> ByteString
193-
storeDirVal = BC.pack $ symbolVal @storeDir Proxy
160+
-- | Render a 'StorePath' as a 'FilePath'.
161+
storePathToFilePath
162+
:: StorePath
163+
-> FilePath
164+
storePathToFilePath = BC.unpack . storePathToRawFilePath
194165

195-
-- | A 'StoreDir' whose value is known at compile time.
196-
--
197-
-- A valid instance of 'KnownStoreDir' should represent a valid path,
198-
-- i.e. all "characters" fit into bytes (as determined by the logic of
199-
-- 'BC.pack') and there are no 0 "characters". Currently this is not
200-
-- enforced, but it should be.
201-
type KnownStoreDir = KnownSymbol
166+
-- | Render a 'StorePath' as a 'Text'.
167+
storePathToText
168+
:: StorePath
169+
-> Text
170+
storePathToText = T.pack . BC.unpack . storePathToRawFilePath
171+
172+
-- | Build `narinfo` suffix from `StorePath` which
173+
-- can be used to query binary caches.
174+
storePathToNarInfo
175+
:: StorePath
176+
-> BC.ByteString
177+
storePathToNarInfo StorePath {..} = BS.concat
178+
[ encodeUtf8 $ encodeBase32 storePathHash
179+
, ".narinfo"
180+
]
181+
182+
-- | Parse `StorePath` from `BC.ByteString`, checking
183+
-- that store directory matches `expectedRoot`.
184+
parsePath
185+
:: FilePath
186+
-> BC.ByteString
187+
-> Either String StorePath
188+
parsePath expectedRoot x =
189+
let
190+
(rootDir, fname) = System.FilePath.splitFileName . BC.unpack $ x
191+
(digestPart, namePart) = T.breakOn "-" $ T.pack fname
192+
digest = decodeBase32 digestPart
193+
name = makeStorePathName . T.drop 1 $ namePart
194+
--rootDir' = dropTrailingPathSeparator rootDir
195+
-- cannot use ^^ as it drops multiple slashes /a/b/// -> /a/b
196+
rootDir' = init rootDir
197+
storeDir = if expectedRoot == rootDir'
198+
then Right rootDir'
199+
else Left $ unwords $ [ "Root store dir mismatch, expected", expectedRoot, "got", rootDir']
200+
in
201+
StorePath <$> digest <*> name <*> storeDir
202+
203+
pathParser :: FilePath -> Parser StorePath
204+
pathParser expectedRoot = do
205+
Data.Attoparsec.Text.Lazy.string (T.pack expectedRoot)
206+
<?> "Store root mismatch" -- e.g. /nix/store
207+
208+
Data.Attoparsec.Text.Lazy.char '/'
209+
<?> "Expecting path separator"
210+
211+
digest <- decodeBase32
212+
<$> Data.Attoparsec.Text.Lazy.takeWhile1 (\c -> c `elem` digits32)
213+
<?> "Invalid Base32 part"
214+
215+
Data.Attoparsec.Text.Lazy.char '-'
216+
<?> "Expecting dash (path name separator)"
217+
218+
c0 <- Data.Attoparsec.Text.Lazy.satisfy (\c -> c /= '.' && validStorePathNameChar c)
219+
<?> "Leading path name character is a dot or invalid character"
220+
221+
rest <- Data.Attoparsec.Text.Lazy.takeWhile validStorePathNameChar
222+
<?> "Path name contains invalid character"
223+
224+
let name = makeStorePathName $ T.cons c0 rest
225+
226+
either fail return
227+
$ StorePath <$> digest <*> name <*> pure expectedRoot

0 commit comments

Comments
 (0)