Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download newest package version from defined repositories #24

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ inputPackages:
- https://raw.githubusercontent.com/insightsengineering/scda/main/DESCRIPTION
- https://raw.githubusercontent.com/insightsengineering/scda.2022/main/DESCRIPTION
- https://gitlab.example.com/api/v4/projects/123456/repository/files/DESCRIPTION/raw?ref=main
# Forward slashes in 'directory/subdirectory/DESCRIPTION' path are replaced by '%2F' due to URL encoding
- https://gitlab.example.com/api/v4/projects/234567/repository/files/directory%2Fsubdirectory%2FDESCRIPTION/raw?ref=main
inputRepositories:
- Bioconductor.BioCsoft=https://bioconductor.org/packages/release/bioc
Expand Down Expand Up @@ -166,7 +167,8 @@ For packages which, according to the input lockfile, should be downloaded from C

The packages can be updated selectively by using the `--updatePackages` flag.

Please note that `renv` might have saved the information in the input lockfile that the package should be downloaded from `CRAN`, `RSPM` or BioConductor repository, but at the same time the definition of that repository in the `renv.lock` header (in the `Repositories` section) might be missing. For such packages `locksmith` will try to check what is the newest available package version at [CRAN](https://cloud.r-project.org).
Please note that `renv` might have saved the information in the input lockfile that a package `P` should be downloaded from `CRAN`, `RSPM` or BioConductor repository, but at the same time the definition of that repository in the `renv.lock` header (in the `Repositories` section) might be missing.
In this case, `locksmith` will replicate seemingly undocumented `renv` behavior: the version of package `P` in the lockfile will be updated to the latest version found in any of the repositories **defined** in the lockfile.

Please also note that `locksmith` will not verify whether the dependencies of some packages have changed - this means that the set of package names present in the lockfile will stay the same.

Expand Down
7 changes: 4 additions & 3 deletions cmd/construct.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"strings"
)

const lowestPossiblePackageVersion = "0.0.0.0"
const lowestPossiblePackageVersion = "0.0.0.0.0"

// ConstructOutputPackageList generates a list of all packages and their dependencies
// which should be included in the output renv.lock file,
Expand Down Expand Up @@ -286,8 +286,9 @@ func CheckIfVersionSufficient(availableVersionValue string, versionOperator stri
requiredVersion := stringsToInts(requiredVersionStrings)

available := "="
// Compare up to 4 dot- or dash-separated version components.
for i := 0; i < 4; i++ {
// Compare up to 5 dot- or dash-separated version components.
walkowif marked this conversation as resolved.
Show resolved Hide resolved
// Examples of packages with 5 version components: RcppEigen, RcppArmadillo.
for i := 0; i < 5; i++ {
breakLoop := false
switch {
case availableVersion[i] > requiredVersion[i]:
Expand Down
54 changes: 36 additions & 18 deletions cmd/renv.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,28 @@ func UpdateGitPackages(renvLock *RenvLock, updatePackageRegexp string,
}
}

// GetLatestPackageVersionFromAnyRepository searches for the latest version of soughtPackageName
// in packagesFiles. It returns the name of the repository (as defined in renv.lock header)
// where the latest version of that package has been found.
func GetLatestPackageVersionFromAnyRepository(soughtPackageName string, packagesFiles map[string]PackagesFile) string {
latestPackageVersion := lowestPossiblePackageVersion
latestPackageVersionRepository := ""
for repositoryName, p := range packagesFiles {
for _, packageDescription := range p.Packages {
if packageDescription.Package == soughtPackageName {
log.Trace(soughtPackageName, " version ", packageDescription.Version, " found in ", repositoryName, " repository.")
if CheckIfVersionSufficient(packageDescription.Version, ">", latestPackageVersion) {
latestPackageVersion = packageDescription.Version
latestPackageVersionRepository = repositoryName
}
break
}
}
}
log.Trace("Latest version ", latestPackageVersion, " for package ", soughtPackageName, " found in ", latestPackageVersionRepository, " repository.")
return latestPackageVersionRepository
}

// UpdateRepositoryPackages iterates through the packages in renv.lock and updates the entries
// corresponding to packages downloaded from CRAN-like repositories. Package version is updated
// in the renvLock struct. Only packages matching the updatePackageRegexp are updated.
Expand All @@ -222,19 +244,21 @@ func UpdateRepositoryPackages(renvLock *RenvLock, updatePackageRegexp string,
log.Trace("Package ", k, " matches updated packages regexp ",
updatePackageRegexp)
var repositoryPackagesFile PackagesFile
var notFoundRepositoryName string
repositoryName := v.Repository
repositoryPackagesFile, ok := packagesFiles[repositoryName]
if !ok {
log.Error(`Could not retrieve PACKAGES for "`, repositoryName, `" repository `,
`(referenced by `, k, `). Attempting to use CRAN's PACKAGES as a fallback.`)
repositoryPackagesFile = packagesFiles["CRAN"]
repositoryName = "CRAN"
// Package coming from a repository not defined in the lockfile.
// Check which of the defined repositories has the latest version of that package.
notFoundRepositoryName = repositoryName
repositoryName = GetLatestPackageVersionFromAnyRepository(k, packagesFiles)
repositoryPackagesFile = packagesFiles[repositoryName]
}
var newPackageVersion string
for _, singlePackage := range repositoryPackagesFile.Packages {
if singlePackage.Package == k {
newPackageVersion = singlePackage.Version
continue
break
}
}
if newPackageVersion == "" {
Expand All @@ -243,6 +267,13 @@ func UpdateRepositoryPackages(renvLock *RenvLock, updatePackageRegexp string,
}
if entry, ok := renvLock.Packages[k]; ok {
if newPackageVersion != entry.Version {
if notFoundRepositoryName != "" {
log.Warn(
"Repository ", notFoundRepositoryName, " referenced by package ", k, " has not ",
"been defined in the lockfile and ", k, " will be updated to the latest version ",
`found in "`, repositoryName, `" repository.`,
)
}
log.Info("Updating package ", k, " version: ",
entry.Version, " → ", newPackageVersion)
entry.Version = newPackageVersion
Expand All @@ -262,19 +293,6 @@ func GetPackagesFiles(renvLock RenvLock) map[string]PackagesFile {
packagesFile := ProcessPackagesFile(packagesFileContent)
repositoryPackagesFiles[repository.Name] = packagesFile
}

// Check if the PACKAGES file from a repository named CRAN has been downloaded.
_, ok := repositoryPackagesFiles["CRAN"]
if !ok {
// If not, save CRAN's PACKAGES file to be used as a fallback, for packages which
// (according to renv.lock) should be downloaded from a repository not defined in
// the renv.lock header.
_, _, cranPackagesContent := DownloadTextFile(
"https://cloud.r-project.org/src/contrib/PACKAGES", make(map[string]string),
)
cranPackagesFile := ProcessPackagesFile(cranPackagesContent)
repositoryPackagesFiles["CRAN"] = cranPackagesFile
}
return repositoryPackagesFiles
}

Expand Down
20 changes: 14 additions & 6 deletions cmd/renv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,12 @@ func Test_UpdateRepositoryPackages(t *testing.T) {
"", "", []Dependency{},
"", "", "", "", "", "", "", []string{},
},
{
"package19",
"5.2.1",
"", "", []Dependency{},
"", "", "", "", "", "", "", []string{},
},
},
}
packagesFiles["Repo2"] = PackagesFile{
Expand All @@ -378,6 +384,12 @@ func Test_UpdateRepositoryPackages(t *testing.T) {
"", "", []Dependency{},
"", "", "", "", "", "", "", []string{},
},
{
"package19",
"5.2.2",
"", "", []Dependency{},
"", "", "", "", "", "", "", []string{},
},
},
}
packagesFiles["Repo3"] = PackagesFile{
Expand All @@ -388,13 +400,9 @@ func Test_UpdateRepositoryPackages(t *testing.T) {
"", "", []Dependency{},
"", "", "", "", "", "", "", []string{},
},
},
}
packagesFiles["CRAN"] = PackagesFile{
[]PackageDescription{
{
"package19",
"5.2.3",
"5.2.2.4",
"", "", []Dependency{},
"", "", "", "", "", "", "", []string{},
},
Expand All @@ -407,6 +415,6 @@ func Test_UpdateRepositoryPackages(t *testing.T) {
assert.Equal(t, renvLock.Packages["package16"].Version, "1.2.3")
assert.Equal(t, renvLock.Packages["package17"].Version, "1.1.1")
assert.Equal(t, renvLock.Packages["package18"].Version, "2.3.2")
assert.Equal(t, renvLock.Packages["package19"].Version, "5.2.3")
assert.Equal(t, renvLock.Packages["package19"].Version, "5.2.2.4")
assert.Equal(t, renvLock.Packages["package21"].Version, "3.8.1")
}
21 changes: 11 additions & 10 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func setLogLevel() {
log.SetFormatter(customFormatter)
log.SetReportCaller(false)
customFormatter.FullTimestamp = false
fmt.Println("logLevel =", logLevel)
fmt.Println(`logLevel = "` + logLevel + `"`)
switch logLevel {
case "trace":
log.SetLevel(logrus.TraceLevel)
Expand All @@ -77,6 +77,7 @@ func setLogLevel() {

var rootCmd *cobra.Command

//nolint:revive
func newRootCommand() {
rootCmd = &cobra.Command{
Use: "locksmith",
Expand All @@ -93,15 +94,15 @@ in an renv.lock-compatible file.`,
Run: func(cmd *cobra.Command, args []string) {
setLogLevel()

fmt.Println("config =", cfgFile)
fmt.Println("inputPackageList =", inputPackageList)
fmt.Println("inputRepositoryList =", inputRepositoryList)
fmt.Println(`config = "` + cfgFile + `"`)
fmt.Println(`inputPackageList = "` + inputPackageList + `"`)
fmt.Println(`inputRepositoryList = "` + inputRepositoryList + `"`)
fmt.Println("inputPackages =", inputPackages)
fmt.Println("inputRepositories =", inputRepositories)
fmt.Println("inputRenvLock =", inputRenvLock)
fmt.Println("outputRenvLock =", outputRenvLock)
fmt.Println("allowIncompleteRenvLock =", allowIncompleteRenvLock)
fmt.Println("updatePackages =", updatePackages)
fmt.Println(`inputRenvLock = "` + inputRenvLock + `"`)
fmt.Println(`outputRenvLock = "` + outputRenvLock + `"`)
fmt.Println(`allowIncompleteRenvLock = "` + allowIncompleteRenvLock + `"`)
fmt.Println(`updatePackages = "` + updatePackages + `"`)

if runtime.GOOS == "windows" {
localTempDirectory = os.Getenv("TMP") + `\tmp\locksmith`
Expand Down Expand Up @@ -138,7 +139,7 @@ in an renv.lock-compatible file.`,
"Token to download non-public files from GitLab.")
rootCmd.PersistentFlags().StringVarP(&inputRenvLock, "inputRenvLock", "n", "",
"Lockfile which should be read and updated to include the newest versions of the packages.")
rootCmd.PersistentFlags().StringVarP(&outputRenvLock, "outputRenvLock", "o", "renv.lock",
rootCmd.PersistentFlags().StringVarP(&outputRenvLock, "outputRenvLock", "k", "renv.lock",
"File name to save the output renv.lock file.")
rootCmd.PersistentFlags().StringVarP(&allowIncompleteRenvLock, "allowIncompleteRenvLock", "i", "",
"Locksmith will fail if any of dependencies of input packages cannot be found in the repositories. "+
Expand Down Expand Up @@ -173,7 +174,7 @@ func initConfig() {
home, err := os.UserHomeDir()
cobra.CheckErr(err)

// Search config in home directory with name ".locksmith" (without extension).
// Search for config in home directory.
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".locksmith")
Expand Down
Loading