Skip to content

Commit 8177cd3

Browse files
i-am-the-slimethomashoneymanf-f
authored
Support exclude files (#613)
* Allow possibility to exclude files * Add test for exclude functionality * Take files and excludeFiles into account when converting spago config to manifest * Rename excludedFiles to excludeFiles * Don't error out when excluding files outside of package directory * Ensure mandatory files are always included * Rename files and excludeFiles to include and exclude * Use excludeFiles / includeFiles * Add links to always-ignored and always-included files * Apply suggestions from code review Co-authored-by: Fabrizio Ferrai <[email protected]> --------- Co-authored-by: Thomas Honeyman <[email protected]> Co-authored-by: Fabrizio Ferrai <[email protected]>
1 parent 64e3a6c commit 8177cd3

File tree

13 files changed

+137
-62
lines changed

13 files changed

+137
-62
lines changed

README.md

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -112,37 +112,6 @@ All the pre-registry packages will be grandfathered in, see [here for details](#
112112
You can find some examples of the `Manifest` that has been generated for them in
113113
the [examples](./examples) folder.
114114

115-
Here we embed a copy of the `Manifest` schema for ease of consultation:
116-
117-
```dhall
118-
{-
119-
120-
The schema for `purs.json` files.
121-
122-
This object holds all the info that the Registry needs to know about it.
123-
124-
-}
125-
126-
let Map = (./Prelude.dhall).Map.Type
127-
128-
let Manifest =
129-
-- The name of the package
130-
{ name : Text
131-
-- A short description of the package
132-
, description : Optional Text
133-
-- The SPDX code for the license under which the code is released
134-
, license : Text
135-
-- The version of this package
136-
, version : Text
137-
-- The location where package sources can be found
138-
, location : ./Location.dhall
139-
-- The packages this package depends on
140-
, dependencies : Map Text Text
141-
}
142-
143-
in Manifest
144-
```
145-
146115
Note: the [`Location` schema](./v1/Location.dhall) includes support for packages that are
147116
not published from the root of the repository, by supplying the (optional) `subdir` field.
148117
This means that a repository could potentially host several packages (commonly called a "monorepo").

SPEC.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,15 @@ All packages in the registry contain a `purs.json` manifest file in their root d
199199
- `location`: a valid [`Location`](#location)
200200
- `owners` (optional): a non-empty array of [`Owner`](#owner)
201201
- `description` (optional): a description of your library as a plain text string, not markdown, up to 300 characters
202-
- `files` (optional): a non-empty array of globs, where globs are used to match files outside the `src` directory you want included in your package tarball
203-
- Globs must contain only `*`, `**`, `/`, `.`, `..`, and characters for Linux file paths. It is not possible to negate a glob (ie. the `!` character), and globs cannot represent a path out of the package source directory.
202+
- `includeFiles` (optional): a non-empty array of globs, where globs are used to match file paths (in addition to the `src` directory and other [always-included files](#always-included-files)) that you want included in your package tarball
203+
- `excludeFiles` (optional): a non-empty array of globs, where globs are used to match file paths in your package source to exclude them (in addition to the [always-ignored files](#always-ignored-files)) from your package tarball.
204204
- `dependencies`: dependencies of your package as key-value pairs where the keys are [`PackageName`](#packagename)s and values are [`Range`](#range)s; this is a required field, but if you have no dependencies you can provide an empty object.
205-
- All dependencies you provide must exist in the registry, and the dependency ranges must be solvable (ie. it must be possible to produce a single version of each dependency that satisfies the provided version bounds, including any transitive dependencies).
205+
206+
Note:
207+
208+
- Globs you provide at the `includeFiles` and `excludeFiles` keys must contain only `*`, `**`, `/`, `.`, `..`, and characters for Linux file paths. It is not possible to negate a glob (ie. the `!` character), and globs cannot represent a path out of the package source directory.
209+
- When packaging your project source, the registry will first "include" your `src` directory and always-included files such as your `purs.json` file. Then it will include files which match globs indicated by the `includeFiles` key ([always-ignored files](#always-ignored-files) cannot be included). Finally, it will apply the excluding globs indicated by the `excludeFiles` key to the included files ([always-included files](#always-included-files) cannot be excluded).
210+
- Dependencies you provide at the `dependencies` key must exist in the registry, and the dependency ranges must be solvable (ie. it must be possible to produce a single version of each dependency that satisfies the provided version bounds, including any transitive dependencies).
206211

207212
For example:
208213

@@ -216,7 +221,8 @@ For example:
216221
"githubOwner": "purescript",
217222
"githubRepo": "purescript-control"
218223
},
219-
"files": ["test/**/*.purs"],
224+
"include": ["test/**/*.purs"],
225+
"exclude": ["test/graphs"],
220226
"dependencies": { "newtype": ">=3.0.0 <4.0.0", "prelude": ">=4.0.0 <5.0.0" }
221227
}
222228
```
@@ -408,12 +414,16 @@ Finally, the registry will perform some processing on the source code, package t
408414
3. Always-ignored files (listed below) are removed from the source code.
409415
4. The remaining code is packaged into a tarball. The tarball MUST NOT exceed 2,000kb, and a warning will be issued for packages over 200kb.
410416

417+
###### Always-Included Files
418+
411419
These files are always included in the tarball, if present:
412420

413421
- The full contents of the `src` directory.
414422
- The `purs.json`, `spago.yaml`, `spago.dhall` and `packages.dhall`, `bower.json`, and `package.json` manifest formats (in the root of the package).
415423
- Any README or LICENSE files (in the root of the package).
416424

425+
###### Always-Ignored Files
426+
417427
These files are always excluded from the tarball, regardless of what is specified in the `files` key:
418428

419429
- `.psci`, `.psci_modules`, `.spago`, `node_modules`, `bower_components`, `.git`, `CVS`, `.svn`, and `.hg` directories.

app/src/App/API.purs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import Data.FoldableWithIndex (foldMapWithIndex)
2828
import Data.Map as Map
2929
import Data.Newtype (over, unwrap)
3030
import Data.Number.Format as Number.Format
31+
import Data.Set.NonEmpty as NonEmptySet
3132
import Data.String as String
32-
import Data.String.NonEmpty as NonEmptyString
33+
import Data.String.NonEmpty (fromString) as NonEmptyString
34+
import Data.String.NonEmpty.Internal (toString) as NonEmptyString
3335
import Data.String.Regex as Regex
3436
import Effect.Aff as Aff
3537
import Effect.Ref as Ref
@@ -76,7 +78,7 @@ import Registry.Manifest as Manifest
7678
import Registry.Metadata as Metadata
7779
import Registry.Operation (AuthenticatedData, AuthenticatedPackageOperation(..), PackageSetUpdateData, PublishData)
7880
import Registry.Operation as Operation
79-
import Registry.Operation.Validation (UnpublishError(..))
81+
import Registry.Operation.Validation (UnpublishError(..), validateNoExcludedObligatoryFiles)
8082
import Registry.Operation.Validation as Operation.Validation
8183
import Registry.Owner as Owner
8284
import Registry.PackageName as PackageName
@@ -590,7 +592,7 @@ publishRegistry { source, payload, metadata: Metadata metadata, manifest: Manife
590592
-- We copy over all files that are always included (ie. src dir, purs.json file),
591593
-- and any files the user asked for via the 'files' key, and remove all files
592594
-- that should never be included (even if the user asked for them).
593-
copyPackageSourceFiles manifest.files { source: packageDirectory, destination: packageSourceDir }
595+
copyPackageSourceFiles { includeFiles: manifest.includeFiles, excludeFiles: manifest.excludeFiles, source: packageDirectory, destination: packageSourceDir }
594596
Log.debug "Removing always-ignored files from the packaging directory."
595597
removeIgnoredTarballFiles packageSourceDir
596598

@@ -899,33 +901,51 @@ formatPursuitResolutions { resolutions, dependenciesDir } =
899901
-- | Copy files from the package source directory to the destination directory
900902
-- | for the tarball. This will copy all always-included files as well as files
901903
-- | provided by the user via the `files` key.
904+
-- | Finally, it removes any files specified in the globs behind the `excludeFiles` key.
902905
copyPackageSourceFiles
903906
:: forall r
904-
. Maybe (NonEmptyArray NonEmptyString)
905-
-> { source :: FilePath, destination :: FilePath }
907+
. { includeFiles :: Maybe (NonEmptyArray NonEmptyString)
908+
, excludeFiles :: Maybe (NonEmptyArray NonEmptyString)
909+
, source :: FilePath
910+
, destination :: FilePath
911+
}
906912
-> Run (LOG + EXCEPT String + AFF + EFFECT + r) Unit
907-
copyPackageSourceFiles files { source, destination } = do
913+
copyPackageSourceFiles { includeFiles, excludeFiles, source, destination } = do
908914
Log.debug $ "Copying package source files from " <> source <> " to " <> destination
909915

910-
userFiles <- case files of
916+
userExcludeFiles <- case excludeFiles of
911917
Nothing -> pure []
912918
Just nonEmptyGlobs -> do
913919
let globs = map NonEmptyString.toString $ NonEmptyArray.toArray nonEmptyGlobs
914920
{ succeeded, failed } <- FastGlob.match source globs
921+
-- Since we will only subtract the excluded globs we can safely ignore the failed globs
922+
Log.warn $ "The following paths matched by globs in the 'exclude' key are outside your package directory: " <> String.joinWith ", " failed
923+
case validateNoExcludedObligatoryFiles succeeded of
924+
Right _ -> pure succeeded
925+
Left removedObligatoryFiles -> do
926+
Log.warn $ "The following paths matched by globs in the 'excludeFiles' key will be included in the tarball and cannot be excluded: " <> String.joinWith ", " (NonEmptySet.toUnfoldable removedObligatoryFiles)
927+
pure $ succeeded Array.\\ NonEmptySet.toUnfoldable removedObligatoryFiles
928+
929+
userFiles <- case includeFiles of
930+
Nothing -> pure []
931+
Just nonEmptyGlobs -> do
932+
let globs = map NonEmptyString.toString $ NonEmptyArray.toArray nonEmptyGlobs
933+
{ succeeded, failed } <- FastGlob.match source globs
934+
let succeededAndNotExcluded = succeeded Array.\\ userExcludeFiles
915935

916936
unless (Array.null failed) do
917937
Except.throw $ String.joinWith " "
918-
[ "Some paths matched by globs in the 'files' key are outside your package directory."
938+
[ "Some paths matched by globs in the 'includeFiles' key are outside your package directory."
919939
, "Please ensure globs only match within your package directory, including symlinks."
920940
]
921941

922-
case NonEmptyArray.fromArray (Array.filter (Regex.test Internal.Path.pursFileExtensionRegex) succeeded) of
942+
case NonEmptyArray.fromArray (Array.filter (Regex.test Internal.Path.pursFileExtensionRegex) succeededAndNotExcluded) of
923943
Nothing -> pure unit
924944
Just matches -> do
925945
let fullPaths = map (\path -> Path.concat [ source, path ]) matches
926946
Operation.Validation.validatePursModules fullPaths >>= case _ of
927947
Left formattedError ->
928-
Except.throw $ "Some PureScript modules listed in the 'files' section of your manifest contain malformed or disallowed module names." <> formattedError
948+
Except.throw $ "Some PureScript modules listed in the 'includeFiles' section of your manifest contain malformed or disallowed module names." <> formattedError
929949
Right _ ->
930950
pure unit
931951

@@ -934,7 +954,7 @@ copyPackageSourceFiles files { source, destination } = do
934954
includedFiles <- FastGlob.match source includedGlobs
935955
includedInsensitiveFiles <- FastGlob.match' source includedInsensitiveGlobs { caseSensitive: false }
936956

937-
let filesToCopy = userFiles <> includedFiles.succeeded <> includedInsensitiveFiles.succeeded
957+
let filesToCopy = (userFiles <> includedFiles.succeeded <> includedInsensitiveFiles.succeeded) Array.\\ userExcludeFiles
938958
let makePaths path = { from: Path.concat [ source, path ], to: Path.concat [ destination, path ], preserveTimestamps: true }
939959

940960
case map makePaths filesToCopy of
@@ -987,6 +1007,8 @@ spagoToManifest :: Spago.Config -> Either String Manifest
9871007
spagoToManifest config = do
9881008
package@{ name, description, dependencies: Spago.Dependencies deps } <- note "Did not find a package in the config" config.package
9891009
publishConfig@{ version, license } <- note "Did not find a `publish` section in the package config" package.publish
1010+
let includeFiles = NonEmptyArray.fromArray =<< (Array.mapMaybe NonEmptyString.fromString <$> publishConfig.include)
1011+
let excludeFiles = NonEmptyArray.fromArray =<< (Array.mapMaybe NonEmptyString.fromString <$> publishConfig.exclude)
9901012
location <- note "Did not find a `location` field in the publish config" publishConfig.location
9911013
let
9921014
checkRange :: Tuple PackageName (Maybe Range) -> Either PackageName (Tuple PackageName Range)
@@ -1005,5 +1027,6 @@ spagoToManifest config = do
10051027
, description
10061028
, dependencies
10071029
, owners: Nothing -- TODO Spago still needs to add this to its config
1008-
, files: Nothing -- TODO Spago still needs to add this to its config
1030+
, includeFiles
1031+
, excludeFiles
10091032
}

app/src/App/Legacy/Manifest.purs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ type LegacyManifest =
5959

6060
toManifest :: PackageName -> Version -> Location -> LegacyManifest -> Manifest
6161
toManifest name version location { license, description, dependencies } = do
62-
let files = Nothing
62+
let includeFiles = Nothing
63+
let excludeFiles = Nothing
6364
let owners = Nothing
64-
Manifest { name, version, location, license, description, dependencies, files, owners }
65+
Manifest { name, version, location, license, description, dependencies, includeFiles, excludeFiles, owners }
6566

6667
-- | Attempt to retrieve a license, description, and set of dependencies from a
6768
-- | PureScript repo that does not have a Registry-supported manifest, but does

app/test/App/API.purs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ copySourceFiles = Spec.hoistSpec identity (\_ -> Assert.Run.runBaseEffects) $ Sp
216216
writeDirectories (goodDirectories <> Constants.ignoredDirectories <> [ "test" ])
217217
writeFiles (goodFiles <> Constants.ignoredFiles <> [ Path.concat [ "test", "Main.purs" ] ])
218218

219-
API.copyPackageSourceFiles Nothing { source, destination }
219+
API.copyPackageSourceFiles { includeFiles: Nothing, excludeFiles: Nothing, source, destination }
220220

221221
paths <- FastGlob.match destination [ "**/*" ]
222222

@@ -232,21 +232,60 @@ copySourceFiles = Spec.hoistSpec identity (\_ -> Assert.Run.runBaseEffects) $ Sp
232232

233233
Spec.it "Copies user-specified files" \{ source, destination, writeDirectories, writeFiles } -> do
234234
let
235-
userFiles = NonEmptyArray.fromArray =<< sequence [ NonEmptyString.fromString "test/**/*.purs" ]
235+
includeFiles = NonEmptyArray.fromArray =<< sequence [ NonEmptyString.fromString "test/**/*.purs" ]
236236
testDir = [ "test" ]
237237
testFiles = [ Path.concat [ "test", "Main.purs" ], Path.concat [ "test", "Test.purs" ] ]
238238

239239
writeDirectories (goodDirectories <> testDir)
240240
writeFiles (goodFiles <> testFiles)
241241

242-
API.copyPackageSourceFiles userFiles { source, destination }
242+
API.copyPackageSourceFiles { includeFiles, excludeFiles: Nothing, source, destination }
243243

244244
paths <- FastGlob.match destination [ "**/*" ]
245245

246246
let acceptedPaths = goodDirectories <> goodFiles <> testDir <> testFiles
247247

248248
for_ acceptedPaths \path -> do
249249
paths.succeeded `Assert.Run.shouldContain` path
250+
251+
Spec.it "Does not copy user-specified excluded files" \{ source, destination, writeDirectories, writeFiles } -> do
252+
let
253+
includeFiles = NonEmptyArray.fromArray =<< sequence [ NonEmptyString.fromString "test/**/*.purs" ]
254+
excludeFiles = NonEmptyArray.fromArray =<< sequence [ NonEmptyString.fromString "test/**/Test.purs" ]
255+
testDir = [ "test" ]
256+
testMain = Path.concat [ "test", "Main.purs" ]
257+
testTest = Path.concat [ "test", "Test.purs" ]
258+
testFiles = [ testMain, testTest ]
259+
260+
writeDirectories (goodDirectories <> testDir)
261+
writeFiles (goodFiles <> testFiles)
262+
263+
API.copyPackageSourceFiles { includeFiles, excludeFiles, source, destination }
264+
265+
paths <- FastGlob.match destination [ "**/*" ]
266+
267+
let acceptedPaths = goodDirectories <> goodFiles <> testDir <> [ testMain ]
268+
269+
for_ acceptedPaths \path -> do
270+
paths.succeeded `Assert.Run.shouldContain` path
271+
272+
Spec.it "Won't exclude always included files" \{ source, destination, writeDirectories, writeFiles } -> do
273+
let
274+
includeFiles = NonEmptyArray.fromArray =<< sequence [ NonEmptyString.fromString "test/**/*.purs" ]
275+
excludeFiles = Just $ NonEmptyArray.singleton (NonEmptyString.nes (Proxy :: _ "purs.json"))
276+
277+
writeDirectories (goodDirectories)
278+
writeFiles (goodFiles)
279+
280+
API.copyPackageSourceFiles { includeFiles, excludeFiles, source, destination }
281+
282+
paths <- FastGlob.match destination [ "**/*" ]
283+
284+
let acceptedPaths = goodDirectories <> goodFiles
285+
286+
for_ acceptedPaths \path -> do
287+
paths.succeeded `Assert.Run.shouldContain` path
288+
250289
where
251290
runBefore :: forall r. Run (EFFECT + r) _
252291
runBefore = do

lib/src/Manifest.purs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ newtype Manifest = Manifest
5252
, location :: Location
5353
, owners :: Maybe (NonEmptyArray Owner)
5454
, description :: Maybe String
55-
, files :: Maybe (NonEmptyArray NonEmptyString)
55+
, includeFiles :: Maybe (NonEmptyArray NonEmptyString)
56+
, excludeFiles :: Maybe (NonEmptyArray NonEmptyString)
5657
, dependencies :: Map PackageName Range
5758
}
5859

@@ -79,6 +80,7 @@ codec = Profunctor.wrapIso Manifest $ CA.object "Manifest"
7980
$ CA.recordPropOptional (Proxy :: _ "description") (Internal.Codec.limitedString 300)
8081
$ CA.recordProp (Proxy :: _ "location") Location.codec
8182
$ CA.recordPropOptional (Proxy :: _ "owners") (CA.Common.nonEmptyArray Owner.codec)
82-
$ CA.recordPropOptional (Proxy :: _ "files") (CA.Common.nonEmptyArray CA.Common.nonEmptyString)
83+
$ CA.recordPropOptional (Proxy :: _ "includeFiles") (CA.Common.nonEmptyArray CA.Common.nonEmptyString)
84+
$ CA.recordPropOptional (Proxy :: _ "excludeFiles") (CA.Common.nonEmptyArray CA.Common.nonEmptyString)
8385
$ CA.recordProp (Proxy :: _ "dependencies") (Internal.Codec.packageMap Range.codec)
8486
$ CA.record

lib/src/Operation/Validation.purs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import Data.Either (Either(..))
1111
import Data.List.NonEmpty (NonEmptyList)
1212
import Data.Map (Map)
1313
import Data.Map as Map
14-
import Data.Maybe (Maybe(..))
14+
import Data.Maybe (Maybe(..), maybe)
1515
import Data.Newtype (un)
16+
import Data.Set (Set)
17+
import Data.Set as Set
18+
import Data.Set.NonEmpty (NonEmptySet)
19+
import Data.Set.NonEmpty as NonEmptySet
1620
import Data.String as String
1721
import Data.Time.Duration (Hours(..))
1822
import Data.Traversable (traverse)
@@ -203,3 +207,24 @@ validatePursModule moduleString = case CST.parsePartialModule moduleString of
203207
Right unit
204208
else
205209
Left $ "Module name is " <> name <> " but PureScript libraries cannot publish modules named: " <> String.joinWith ", " forbiddenModules
210+
211+
-- | These files are always included in the tarball, if present
212+
alwaysIncludedFilesIfPresent :: Set FilePath
213+
alwaysIncludedFilesIfPresent = Set.fromFoldable
214+
[ "purs.json"
215+
, "spago.yaml"
216+
, "spago.dhall"
217+
, "packages.dhall"
218+
, "bower.json"
219+
, "package.json"
220+
]
221+
222+
-- | Validate that the given directory contains no files that are excluded by
223+
-- | the package's configuration. In case of error, return the set of files that
224+
-- | shouldn't be excluded
225+
validateNoExcludedObligatoryFiles :: Array FilePath -> Either (NonEmptySet FilePath) Unit
226+
validateNoExcludedObligatoryFiles files = do
227+
let fileSet = Set.fromFoldable files
228+
let removed = Set.intersection alwaysIncludedFilesIfPresent fileSet
229+
let removedNonEmpty = NonEmptySet.fromSet removed
230+
maybe (Right unit) Left removedNonEmpty

lib/test/Registry/ManifestIndex.purs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,5 +240,6 @@ manifestCodec' = Profunctor.dimap to from $ CA.Record.object "ManifestRep"
240240
}
241241
, description: Nothing
242242
, owners: Nothing
243-
, files: Nothing
243+
, includeFiles: Nothing
244+
, excludeFiles: Nothing
244245
}

lib/test/Registry/Test/Utils.purs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ unsafeManifest name version dependencies = Manifest
106106
}
107107
, description: Nothing
108108
, owners: Nothing
109-
, files: Nothing
109+
, includeFiles: Nothing
110+
, excludeFiles: Nothing
110111
}
111112

112113
-- | Format a package version as a string in the form '[email protected]'

lib/test/_fixtures/manifests/prelude-4.1.1.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
"public": "abc123"
1414
}
1515
],
16-
"files": [
16+
"includeFiles": [
1717
"test/**/*.purs"
1818
],
19+
"excludeFiles": [
20+
"test/**/*Spec.purs"
21+
],
1922
"dependencies": {}
2023
}

0 commit comments

Comments
 (0)