Skip to content

Commit dadf06a

Browse files
authored
Allow incomplete renv.lock (#10)
1 parent e2dce76 commit dadf06a

File tree

8 files changed

+138
-40
lines changed

8 files changed

+138
-40
lines changed

README.md

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44

55
`locksmith` is a utility to generate `renv.lock` file containing all dependencies of given set of R packages.
66

7-
Given the input list of git repositories containing the R packages, as well as a list of R package repositories (e.g. in a package manager, CRAN, BioConductor etc.), `locksmith` will try to determine the list of all dependencies and their versions required to make the input list of packages work. It will then save the result in an `renv.lock`-compatible file.
7+
Given the input list of git repositories containing the R packages, as well as a list of R package
8+
repositories (e.g. in a package manager, CRAN, BioConductor etc.), `locksmith` will try to determine
9+
the list of all dependencies and their versions required to make the input list of packages work.
10+
It will then save the result in an `renv.lock`-compatible file.
11+
12+
For additional information about `renv.lock`, please refer to the [`renv` documentation](https://rstudio.github.io/renv/articles/renv.html).
813

914
## Installation
1015

11-
Simply download the project for your distribution from the [releases](https://github.com/insightsengineering/locksmith/releases) page. `locksmith` is distributed as a single binary file and does not require any additional system requirements.
16+
Simply download the project for your distribution from the
17+
[releases](https://github.com/insightsengineering/locksmith/releases) page. `locksmith` is
18+
distributed as a single binary file and does not need any additional system requirements.
1219

1320
Alternatively, you can install the latest version by running:
1421

@@ -18,7 +25,8 @@ go install github.com/insightsengineering/locksmith@latest
1825

1926
## Usage
2027

21-
`locksmith` is a command line utility, so after installing the binary in your `PATH`, simply run the following command to view its capabilities:
28+
`locksmith` is a command line utility, so after installing the binary in your `PATH`, simply run the
29+
following command to view its capabilities:
2230

2331
```bash
2432
locksmith --help
@@ -31,13 +39,15 @@ locksmith --logLevel debug --exampleParameter 'exampleValue'
3139
```
3240

3341
Real-life example with multiple input packages and repositories.
34-
Please see below for [an example](#configuration-file) how to set package and repository lists more easily in a configuration file.
42+
Please see below for [an example](#configuration-file) how to set package and repository lists more
43+
easily in a configuration file.
3544

3645
```bash
37-
locksmith --inputPackageList https://raw.githubusercontent.com/insightsengineering/formatters/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/rtables/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/scda/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/scda.2022/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/nestcolor/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/tern/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/rlistings/main/DESCRIPTION --inputRepositoryList BioC=https://bioconductor.org/packages/release/bioc,CRAN=https://cran.rstudio.com
46+
locksmith --inputPackageList https://raw.githubusercontent.com/insightsengineering/formatters/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/rtables/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/scda/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/scda.2022/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/nestcolor/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/tern/main/DESCRIPTION,https://raw.githubusercontent.com/insightsengineering/rlistings/main/DESCRIPTION,https://gitlab.example.com/projectgroup/projectsubgroup/projectname/-/raw/main/DESCRIPTION --inputRepositoryList BioC=https://bioconductor.org/packages/release/bioc,CRAN=https://cran.rstudio.com
3847
```
3948

40-
In order to download the packages from GitHub or GitLab repositories, please set the environment variables containing the Personal Access Tokens.
49+
In order to download the packages from non-public GitHub or GitLab repositories, please set the environment
50+
variables containing the Personal Access Tokens.
4151

4252
* For GitHub, set the `LOCKSMITH_GITHUBTOKEN` environment variable.
4353
* For GitLab, set the `LOCKSMITH_GITLABTOKEN` environment variable.
@@ -46,12 +56,15 @@ By default `locksmith` will save the resulting output file to `renv.lock`.
4656

4757
## Configuration file
4858

49-
If you'd like to set the above options in a configuration file, by default `locksmith` checks `~/.locksmith`, `~/.locksmith.yaml` and `~/.locksmith.yml` files.
59+
If you'd like to set the above options in a configuration file, by default `locksmith` checks
60+
`~/.locksmith`, `~/.locksmith.yaml` and `~/.locksmith.yml` files.
5061

51-
If any of these files exist, `locksmith` will use options defined there, unless they are overridden by command line flags or environment variables.
62+
If any of these files exist, `locksmith` will use options defined there, unless they are overridden
63+
by command line flags or environment variables.
5264

53-
You can also specify custom path to configuration file with `--config <your-configuration-file>.yml` command line flag.
54-
When using custom configuration file, if you specify command line flags, the latter will still take precedence.
65+
You can also specify custom path to configuration file with `--config <your-configuration-file>.yml`
66+
command line flag. When using custom configuration file, if you specify command line flags,
67+
the latter will still take precedence.
5568

5669
Example contents of configuration file:
5770

@@ -62,6 +75,7 @@ inputPackages:
6275
- https://raw.githubusercontent.com/insightsengineering/rtables/main/DESCRIPTION
6376
- https://raw.githubusercontent.com/insightsengineering/scda/main/DESCRIPTION
6477
- https://raw.githubusercontent.com/insightsengineering/scda.2022/main/DESCRIPTION
78+
- https://gitlab.example.com/projectgroup/projectsubgroup/projectname/-/raw/main/DESCRIPTION
6579
inputRepositories:
6680
- Bioconductor.BioCsoft=https://bioconductor.org/packages/release/bioc
6781
- CRAN=https://cran.rstudio.com
@@ -70,11 +84,23 @@ inputRepositories:
7084
The example above shows an alternative way of providing input packages, and input repositories,
7185
as opposed to `inputPackageList` and `inputRepositoryList` CLI flags/YAML keys.
7286

73-
Additionally, `inputPackageList`/`inputRepositoryList` CLI flags take precendence over `inputPackages`/`inputRepositories` YAML keys.
87+
Additionally, `inputPackageList`/`inputRepositoryList` CLI flags take precendence over
88+
`inputPackages`/`inputRepositories` YAML keys.
89+
90+
## Environment variables
91+
92+
`locksmith` reads environment variables with `LOCKSMITH_` prefix and tries to match them with CLI
93+
flags. For example, setting the following variables will override the respective values from the
94+
configuration file: `LOCKSMITH_LOGLEVEL`, `LOCKSMITH_INPUTPACKAGELIST`, `LOCKSMITH_INPUTREPOSITORYLIST` etc.
95+
96+
The order of precedence is:
97+
98+
CLI flag → environment variable → configuration file → default value.
7499

75100
## Binary dependencies
76101

77-
For `locksmith` in order to generate an `renv.lock` with binary R packages, it is necessary to provide URLs to binary repositories in `inputRepositories`/`inputRepositoryList`.
102+
For `locksmith` in order to generate an `renv.lock` with binary R packages,
103+
it is necessary to provide URLs to binary repositories via `inputRepositories`/`inputRepositoryList`.
78104

79105
Examples illustrating the expected format of URLs to repositories with binary packages:
80106

@@ -113,23 +139,27 @@ As a result, the configuration file could look like this:
113139
- Bioc-Windows=https://www.bioconductor.org/packages/release/bioc/bin/windows/contrib/4.3
114140
```
115141

116-
## Environment variables
142+
## Packages not found in the repositories
117143

118-
`locksmith` reads environment variables with `LOCKSMITH_` prefix and tries to match them with CLI flags.
119-
For example, setting the following variables will override the respective values from configuration file:
120-
`LOCKSMITH_LOGLEVEL`, `LOCKSMITH_EXAMPLEPARAMETER` etc.
144+
It may happen that some of the dependencies required by the input packages cannot be found in any of
145+
the input repositories. By default, `locksmith` will fail in such case and show a list of such dependencies.
121146

122-
The order of precedence is:
147+
However, it is possible to override this behavior by using the `--allowIncompleteRenvLock` flag.
148+
Simply list the types of dependencies which should not cause the `renv.lock` generation to fail:
123149

124-
CLI flag → environment variable → configuration file → default value.
150+
```bash
151+
locksmith --allowIncompleteRenvLock 'Imports,Depends,Suggests,LinkingTo'
152+
```
125153

126154
## Development
127155

128156
This project is built with the [Go programming language](https://go.dev/).
129157

130158
### Development Environment
131159

132-
It is recommended to use Go 1.21+ for developing this project. This project uses a pre-commit configuration and it is recommended to [install and use pre-commit](https://pre-commit.com/#install) when you are developing this project.
160+
It is recommended to use Go 1.21+ for developing this project. This project uses a pre-commit
161+
configuration and it is recommended to [install and use pre-commit](https://pre-commit.com/#install)
162+
when you are developing this project.
133163

134164
### Common Commands
135165

cmd/construct.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import (
2424
// which should be included in the output renv.lock file,
2525
// based on the list of package descriptions, and information contained in the PACKAGES files.
2626
func ConstructOutputPackageList(packages []PackageDescription, packagesFiles map[string]PackagesFile,
27-
repositoryList []string) []PackageDescription {
27+
repositoryList []string, allowedMissingDependencyTypes []string) []PackageDescription {
2828
var outputPackageList []PackageDescription
2929
var fatalErrors string
30+
var nonFatalErrors string
3031
// Add all input packages to output list, as the packages should be downloaded from git repositories.
3132
for _, p := range packages {
3233
outputPackageList = append(outputPackageList, PackageDescription{
@@ -44,7 +45,8 @@ func ConstructOutputPackageList(packages []PackageDescription, packagesFiles map
4445
log.Info(p.Package, " → ", d.DependencyName, " (", d.DependencyType, ")")
4546
ResolveDependenciesRecursively(
4647
&outputPackageList, d.DependencyName, d.VersionOperator,
47-
d.VersionValue, repositoryList, packagesFiles, 1, &fatalErrors,
48+
d.VersionValue, d.DependencyType, allowedMissingDependencyTypes,
49+
repositoryList, packagesFiles, 1, &fatalErrors, &nonFatalErrors,
4850
)
4951
}
5052
}
@@ -53,6 +55,9 @@ func ConstructOutputPackageList(packages []PackageDescription, packagesFiles map
5355
if fatalErrors != "" {
5456
log.Fatal(fatalErrors)
5557
}
58+
if nonFatalErrors != "" {
59+
log.Error(nonFatalErrors)
60+
}
5661
return outputPackageList
5762
}
5863

@@ -61,8 +66,9 @@ func ConstructOutputPackageList(packages []PackageDescription, packagesFiles map
6166
// (later used to generate the renv.lock), or if the dependency should be downloaded from a package repository.
6267
// Repeats the process recursively for all dependencies not yet processed.
6368
func ResolveDependenciesRecursively(outputList *[]PackageDescription, name string, versionOperator string,
64-
versionValue string, repositoryList []string, packagesFiles map[string]PackagesFile, recursionLevel int,
65-
fatalErrors *string) {
69+
versionValue string, dependencyType string, allowedMissingDependencyTypes []string,
70+
repositoryList []string, packagesFiles map[string]PackagesFile, recursionLevel int,
71+
fatalErrors *string, nonFatalErrors *string) {
6672
var indentation string
6773
for i := 0; i < recursionLevel; i++ {
6874
indentation += " "
@@ -103,7 +109,8 @@ func ResolveDependenciesRecursively(outputList *[]PackageDescription, name strin
103109
)
104110
ResolveDependenciesRecursively(
105111
outputList, d.DependencyName, d.VersionOperator, d.VersionValue,
106-
repositoryList, packagesFiles, recursionLevel+1, fatalErrors,
112+
d.DependencyType, allowedMissingDependencyTypes, repositoryList,
113+
packagesFiles, recursionLevel+1, fatalErrors, nonFatalErrors,
107114
)
108115
}
109116
}
@@ -115,9 +122,16 @@ func ResolveDependenciesRecursively(outputList *[]PackageDescription, name strin
115122
}
116123
var versionConstraint string
117124
if versionOperator != "" && versionValue != "" {
118-
versionConstraint = " in version " + versionOperator + " " + versionValue
125+
versionConstraint = " (version " + versionOperator + " " + versionValue + ")"
126+
}
127+
message := "Could not find package " + name + versionConstraint + " in any of the repositories.\n"
128+
if stringInSlice(dependencyType, allowedMissingDependencyTypes) {
129+
log.Warn(indentation + message)
130+
*nonFatalErrors += message
131+
} else {
132+
log.Error(indentation + message)
133+
*fatalErrors += message
119134
}
120-
*fatalErrors += "Could not find package " + name + versionConstraint + " in any of the repositories.\n"
121135
}
122136

123137
// CheckIfBasePackage checks whether the package should be treated as a base R package

cmd/construct_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,12 @@ func Test_ConstructOutputPackageList(t *testing.T) {
326326
"",
327327
"",
328328
},
329+
{
330+
"LinkingTo",
331+
"nonExistentPackage",
332+
"",
333+
"",
334+
},
329335
},
330336
"", "", "", "", "", "", "",
331337
},
@@ -382,6 +388,9 @@ func Test_ConstructOutputPackageList(t *testing.T) {
382388
},
383389
},
384390
packagesFiles, repositoryList,
391+
// Let the generation of renv.lock proceed, despite 'nonExistentPackage'
392+
// (dependency type LinkingTo) not being found in any repository.
393+
[]string{"LinkingTo"},
385394
)
386395
assert.Equal(t, outputPackageList,
387396
[]PackageDescription{

cmd/parse.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,21 @@ func ParsePackagesFiles(repositoryPackageFiles map[string]string) map[string]Pac
4646
// with those fields/properties that are required for further processing.
4747
func ProcessPackagesFile(content string) PackagesFile {
4848
var allPackages PackagesFile
49+
// PACKAGES files in binary Windows repositories use CRLF line endings.
50+
// Therefore, we first change them to LF line endings.
4951
for _, lineGroup := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n\n") {
5052
if lineGroup == "" {
5153
continue
5254
}
5355
// Each lineGroup contains information about one package and is separated by an empty line.
5456
firstLine := strings.Split(lineGroup, "\n")[0]
5557
packageName := strings.ReplaceAll(firstLine, "Package: ", "")
56-
cleaned := CleanDescriptionOrPackagesEntry(lineGroup)
58+
cleaned := CleanDescriptionOrPackagesEntry(lineGroup, false)
59+
if cleaned == "" {
60+
// Package entry pointing to a "Path:" subdirectory encountered.
61+
// Such package entries are skipped altogether.
62+
continue
63+
}
5764
packageMap := make(map[string]string)
5865
err := yaml.Unmarshal([]byte(cleaned), &packageMap)
5966
if err != nil {
@@ -75,7 +82,7 @@ func ProcessPackagesFile(content string) PackagesFile {
7582
// ProcessDescription reads a string containing DESCRIPTION file and returns a structure
7683
// with those fields/properties that are required for further processing.
7784
func ProcessDescription(description DescriptionFile, allPackages *[]PackageDescription) {
78-
cleaned := CleanDescriptionOrPackagesEntry(description.Contents)
85+
cleaned := CleanDescriptionOrPackagesEntry(description.Contents, true)
7986
packageMap := make(map[string]string)
8087
err := yaml.Unmarshal([]byte(cleaned), &packageMap)
8188
checkError(err)
@@ -92,16 +99,24 @@ func ProcessDescription(description DescriptionFile, allPackages *[]PackageDescr
9299
)
93100
}
94101

95-
// CleanDescriptionOrPackagesEntry processes a multiline string representing information about one package
96-
// from PACKAGES file, or the whole contents of DESCRIPTION file. Removes newlines occurring within
97-
// filtered fields (which are predominantly fields containing lists of package dependencies).
98-
// Also removes fields which are not required for further processing.
99-
func CleanDescriptionOrPackagesEntry(description string) string {
102+
// CleanDescriptionOrPackagesEntry processes a multiline string representing information about one
103+
// package from PACKAGES file (if isDescription is false), or the whole contents of DESCRIPTION file
104+
// (if isDescription is true). Removes newlines occurring within filtered fields (which are
105+
// predominantly fields containing lists of package dependencies). Also removes fields which are not
106+
// required for further processing.
107+
func CleanDescriptionOrPackagesEntry(description string, isDescription bool) string {
100108
lines := strings.Split(description, "\n")
101109
filterFields := []string{"Package:", "Version:", "Depends:", "Imports:", "Suggests:", "LinkingTo:"}
102110
outputContent := ""
103111
processingFilteredField := false
104112
for _, line := range lines {
113+
if strings.HasPrefix(line, "Path:") && !isDescription {
114+
// This means that the package is located in a subdirectory mentioned in this field.
115+
// For example "Path: 4.4.0/Recommended" means that the package is located in
116+
// "latest/src/contrib/4.4.0/Recommended/" subdirectory. We want to avoid these kinds of
117+
// packages and prefer to download them from "latest/src/contrib/".
118+
return ""
119+
}
105120
filteredFieldFound := false
106121
// Check if we start processing any of the filtered fields.
107122
for _, field := range filterFields {

cmd/root.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var logLevel string
3333
var gitHubToken string
3434
var gitLabToken string
3535
var outputRenvLock string
36+
var allowIncompleteRenvLock string
3637

3738
// In case the lists are provided as arrays in YAML configuration file:
3839
var inputPackages []string
@@ -93,13 +94,14 @@ in an renv.lock-compatible file.`,
9394
fmt.Println("inputPackages =", inputPackages)
9495
fmt.Println("inputRepositories =", inputRepositories)
9596
fmt.Println("outputRenvLock =", outputRenvLock)
97+
fmt.Println("allowIncompleteRenvLock =", allowIncompleteRenvLock)
9698

97-
packageDescriptionList, repositoryList, repositoryMap := ParseInput()
99+
packageDescriptionList, repositoryList, repositoryMap, allowedMissingDependencyTypes := ParseInput()
98100
inputDescriptionFiles := DownloadDescriptionFiles(packageDescriptionList, DownloadTextFile)
99101
inputPackages := ParseDescriptionFileList(inputDescriptionFiles)
100102
repositoryPackagesFiles := DownloadPackagesFiles(repositoryList, DownloadTextFile)
101103
packagesFiles := ParsePackagesFiles(repositoryPackagesFiles)
102-
outputPackageList := ConstructOutputPackageList(inputPackages, packagesFiles, repositoryList)
104+
outputPackageList := ConstructOutputPackageList(inputPackages, packagesFiles, repositoryList, allowedMissingDependencyTypes)
103105
renvLock := GenerateRenvLock(outputPackageList, repositoryMap)
104106
writeJSON(outputRenvLock, renvLock)
105107
},
@@ -118,6 +120,10 @@ in an renv.lock-compatible file.`,
118120
"Token to download non-public files from GitLab.")
119121
rootCmd.PersistentFlags().StringVar(&outputRenvLock, "outputRenvLock", "renv.lock",
120122
"File name to save the output renv.lock file.")
123+
rootCmd.PersistentFlags().StringVar(&allowIncompleteRenvLock, "allowIncompleteRenvLock", "",
124+
"Locksmith will fail if any of dependencies of input packages cannot be found in the repositories. "+
125+
"However, it will not fail for comma-separated dependency types listed in this argument, e.g.: "+
126+
"'Imports,Depends,Suggests,LinkingTo'")
121127

122128
// Add version command.
123129
rootCmd.AddCommand(extension.NewVersionCobraCmd())
@@ -173,6 +179,7 @@ func initializeConfig() {
173179
"gitHubToken",
174180
"gitLabToken",
175181
"outputRenvLock",
182+
"allowIncompleteRenvLock",
176183
} {
177184
// If the flag has not been set in newRootCommand() and it has been set in initConfig().
178185
// In other words: if it's not been provided in command line, but has been

cmd/testdata/PACKAGES

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ License: GPL-3
1313
MD5sum: bbb222333444555666
1414
NeedsCompilation: no
1515

16+
Package: skippedPackage
17+
Version: 5.0.0
18+
Depends: R (>= 3.6.0)
19+
Imports: grDevices, graphics, grid, lattice, stats, utils
20+
License: GPL-3
21+
MD5sum: aaabbbccc999888777
22+
NeedsCompilation: no
23+
Path: 4.4.0/Recommended
24+
1625
Package: somePackage3
1726
Version: 0.0.1
1827
Depends: R (>= 3.1.0)

0 commit comments

Comments
 (0)