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

Support .Net C# projects using dotnet cli and nuget.org central repository. #62

Merged
merged 28 commits into from
Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5c0ef62
Start to develop dotnet
navteniev Jan 22, 2021
dfe4851
Lookup specfile from project directory
navteniev Jan 22, 2021
9f3e528
Added support for list command
navteniev Mar 19, 2021
5ea596d
Adding info capabilities
navteniev Mar 19, 2021
5700d3d
"additional package details"
navteniev Mar 19, 2021
468bea0
removed debug test, added support for list all
navteniev Apr 6, 2021
29f5b7f
implemented addPackages
navteniev Apr 6, 2021
5b32e4f
implemented search
navteniev Apr 7, 2021
9fbb7b1
Implemented Remove, Install, Lock, and set Quirks
navteniev Apr 8, 2021
4142787
Refactored code for testing, added tests
navteniev Apr 9, 2021
167f555
added documentation
navteniev Apr 9, 2021
2720eb0
Split functions into seperate files
navteniev Apr 9, 2021
737f66e
Split tests into seperate files, added more tests.
navteniev Apr 9, 2021
ef88067
added api doc link
navteniev Apr 9, 2021
11f96e7
Handle project references in lock file
navteniev Apr 10, 2021
79b041a
fixed typo in error message
navteniev Jun 25, 2021
7e177e6
Using a struct to parse the lock file data.
navteniev Jun 25, 2021
32f36f1
Changed language to be dotnet
navteniev Jun 30, 2021
6a0bf5f
Use query escape
navteniev Jul 2, 2021
0f138d0
Include actual error in output
navteniev Jul 2, 2021
8acb944
Use url.PathEscape with packageName
navteniev Jul 2, 2021
a313f9e
Avoid time of check time of use race condition
navteniev Jul 2, 2021
ad5059d
Use appropriate documentation format
navteniev Jul 2, 2021
ae02579
Include actual error in output
navteniev Jul 2, 2021
97cbaa5
Included err in message and used pathescape
navteniev Jul 2, 2021
ad08163
Fix docstrings and bypass TOCTOU race condition.
navteniev Jul 2, 2021
de8e6b1
print unescaped latest version
navteniev Jul 2, 2021
97eafaf
Include error in message
navteniev Jul 2, 2021
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
2 changes: 2 additions & 0 deletions internal/backends/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/replit/upm/internal/api"
"github.com/replit/upm/internal/backends/dart"
"github.com/replit/upm/internal/backends/dotnet"
"github.com/replit/upm/internal/backends/elisp"
"github.com/replit/upm/internal/backends/java"
"github.com/replit/upm/internal/backends/nodejs"
Expand All @@ -31,6 +32,7 @@ var languageBackends = []api.LanguageBackend{
dart.DartPubBackend,
java.JavaBackend,
rlang.RlangBackend,
dotnet.DotNetBackend,
}

// matchesLanguage checks if a language backend matches a value for
Expand Down
31 changes: 31 additions & 0 deletions internal/backends/dotnet/dotnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Package dotnet provides a backend for c# using dotnet and nuget.org
package dotnet

import (
"github.com/replit/upm/internal/api"
"github.com/replit/upm/internal/util"
)

// DotNetBackend is the UPM language backend .NET languages with support for C#
var DotNetBackend = api.LanguageBackend{
Name: "dotnet",
Specfile: findSpecFile(),
Lockfile: lockFileName,
FilenamePatterns: []string{"*.cs", "*.csproj"}, // C# support so far
Remove: func(pkgs map[api.PkgName]bool) { removePackages(pkgs, findSpecFile(), util.RunCmd) },
Add: func(pkgs map[api.PkgName]api.PkgSpec, projectName string) {
addPackages(pkgs, projectName, util.RunCmd)
},
Search: search,
Info: info,
Install: func() { install(util.RunCmd) },
Lock: func() { lock(util.RunCmd) },
ListSpecfile: listSpecfile,
ListLockfile: listLockfile,
GetPackageDir: func() string {
return "bin/"
},
Quirks: api.QuirksAddRemoveAlsoLocks |
api.QuirksAddRemoveAlsoInstalls |
api.QuirksLockAlsoInstalls,
}
35 changes: 35 additions & 0 deletions internal/backends/dotnet/dotnet_cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dotnet

import (
"github.com/replit/upm/internal/api"
)

// removes packages using dotnet command and updates lock file
func removePackages(pkgs map[api.PkgName]bool, specFileName string, cmdRunner func([]string)) {
for packageName := range pkgs {
command := []string{"dotnet", "remove", specFileName, "package", string(packageName)}
cmdRunner(command)
}
lock(cmdRunner)
}

// adds packages using dotnet command which automatically updates lock files
func addPackages(pkgs map[api.PkgName]api.PkgSpec, projectName string, cmdRunner func([]string)) {
for packageName, spec := range pkgs {
command := []string{"dotnet", "add", "package", string(packageName)}
if string(spec) != "" {
command = append(command, "--version", string(spec))
}
cmdRunner(command)
}
}

// installs all packages using dotnet command
func install(cmdRunner func([]string)) {
cmdRunner([]string{"dotnet", "restore"})
}

// generates or updates the lock file using dotnet command
func lock(cmdRunner func([]string)) {
cmdRunner([]string{"dotnet", "restore", "--use-lock-file"})
}
97 changes: 97 additions & 0 deletions internal/backends/dotnet/dotnet_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package dotnet

import (
"strings"
"testing"

"github.com/replit/upm/internal/api"
)

func TestAddPackages(t *testing.T) {
cmds := []string{}
cmdRunner := func(cmd []string) {
cmds = append(cmds, strings.Join(cmd, " "))
}

addPackages(map[api.PkgName]api.PkgSpec{"package": "1.0"}, "", cmdRunner)

if len(cmds) != 1 {
t.Errorf("Expected one command but got %q", len(cmds))
}

if cmds[0] != "dotnet add package package --version 1.0" {
t.Errorf("Wrong command executed %s", cmds[0])
}
}

func TestAddPackagesWithoutVersion(t *testing.T) {
cmds := []string{}
cmdRunner := func(cmd []string) {
cmds = append(cmds, strings.Join(cmd, " "))
}

addPackages(map[api.PkgName]api.PkgSpec{"package": ""}, "", cmdRunner)

if len(cmds) != 1 {
t.Errorf("Expected one command but got %q", len(cmds))
}

if cmds[0] != "dotnet add package package" {
t.Errorf("Wrong command executed %s", cmds[0])
}
}

func TestRemovePackages(t *testing.T) {
cmds := []string{}
cmdRunner := func(cmd []string) {
cmds = append(cmds, strings.Join(cmd, " "))
}

removePackages(map[api.PkgName]bool{"package": true}, "specFile.csproj", cmdRunner)

if len(cmds) != 2 {
t.Errorf("Expected two command but got %q", len(cmds))
}

if cmds[0] != "dotnet remove specFile.csproj package package" {
t.Errorf("Wrong remove command executed %s", cmds[0])
}

if cmds[1] != "dotnet restore --use-lock-file" {
t.Errorf("Wrong lock command executed %s", cmds[1])
}
}

func TestLock(t *testing.T) {
cmds := []string{}
cmdRunner := func(cmd []string) {
cmds = append(cmds, strings.Join(cmd, " "))
}

lock(cmdRunner)

if len(cmds) != 1 {
t.Errorf("Expected one command but got %q", len(cmds))
}

if cmds[0] != "dotnet restore --use-lock-file" {
t.Errorf("Wrong command executed %s", cmds[0])
}
}

func TestInstall(t *testing.T) {
cmds := []string{}
cmdRunner := func(cmd []string) {
cmds = append(cmds, strings.Join(cmd, " "))
}

install(cmdRunner)

if len(cmds) != 1 {
t.Errorf("Expected one command but got %q", len(cmds))
}

if cmds[0] != "dotnet restore" {
t.Errorf("Wrong command executed %s", cmds[0])
}
}
152 changes: 152 additions & 0 deletions internal/backends/dotnet/nuget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package dotnet

import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"

"github.com/replit/upm/internal/api"
"github.com/replit/upm/internal/util"
)

// Used the documentation provided here https://docs.microsoft.com/en-us/nuget/api/overview

// nuget.org info lookup result
type infoResult struct {
Versions []string `json:"versions"`
}

// nuget.org .nuspec file package repository data
type repository struct {
XMLName xml.Name `xml:"repository"`
Type string `xml:"type,attr"`
URL string `xml:"url,attr"`
Commit string `xml:"commit,attr"`
}

// nuget.org .nuspec file package metadata
type packageMetadata struct {
XMLName xml.Name `xml:"metadata"`
ID string `xml:"id"`
Version string `xml:"version"`
Title string `xml:"title"`
Author string `xml:"author"`
Description string `xml:"description"`
License string `xml:"license"`
Repository repository `xml:"repository"`
ProjectURL string `xml:"projectUrl"`
}

// nuget.org .nuspec file data
type nugetPackage struct {
XMLName xml.Name `xml:"package"`
Metadata packageMetadata `xml:"metadata"`
}

// nuget.org search service result entry
type searchResultData struct {
ID string
Version string
Description string
ProjectURL string
}

// nuget.org search service result record
type searchResult struct {
TotalHits int
Data []searchResultData
}

const searchQueryURL = "https://azuresearch-usnc.nuget.org/query"

// find the first ten projects that match the query string on nuget.org
func search(query string) []api.PkgInfo {
pkgs := []api.PkgInfo{}
queryURL := fmt.Sprintf("%s?q=%s&take=10", searchQueryURL, url.QueryEscape(query))

res, err := http.Get(queryURL)
if err != nil {
util.Die("failed to query for packages: %s", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return pkgs
}

body, err := ioutil.ReadAll(res.Body)
if err != nil {
util.Die("Could not read response: %s", err)
}

var searchResult searchResult
err = json.Unmarshal(body, &searchResult)
if err != nil {
util.Die("Could not unmarshal response data: %s", err)
}

for _, data := range searchResult.Data {
pkgs = append(pkgs, api.PkgInfo{
Name: data.ID,
Version: data.Version,
Description: data.Description,
SourceCodeURL: data.ProjectURL,
})
}

return pkgs
}

// looks up all the versions of the package and gets retails for the latest version from nuget.org
func info(pkgName api.PkgName) api.PkgInfo {
lowID := url.PathEscape(strings.ToLower(string(pkgName)))
infoURL := fmt.Sprintf("https://api.nuget.org/v3-flatcontainer/%s/index.json", lowID)
navteniev marked this conversation as resolved.
Show resolved Hide resolved

res, err := http.Get(infoURL)
if err != nil {
util.Die("failed to get the versions: %s", err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
util.Die("could not read response: %s", err)
}
var infoResult infoResult
err = json.Unmarshal(body, &infoResult)
if err != nil {
util.Die("could not read json body: %s", err)
}
latestVersion := infoResult.Versions[len(infoResult.Versions)-1]
util.ProgressMsg(fmt.Sprintf("latest version of %s is %s", pkgName, latestVersion))
specURL := fmt.Sprintf("https://api.nuget.org/v3-flatcontainer/%s/%s/%s.nuspec", lowID, url.PathEscape(latestVersion), lowID)
util.ProgressMsg(fmt.Sprintf("Getting spec from %s", specURL))
res, err = http.Get(specURL)
if err != nil {
util.Die("failed to get the spec: %s", err)
}
defer res.Body.Close()
body, err = ioutil.ReadAll(res.Body)
if err != nil {
util.Die("could not read response: %s", err)
}
var nugetPackage nugetPackage
err = xml.Unmarshal(body, &nugetPackage)
if err != nil {
util.Die(fmt.Sprintf("failed to read spec %s", err))
}

pkgInfo := api.PkgInfo{
Name: nugetPackage.Metadata.ID,
Version: nugetPackage.Metadata.Version,
Description: nugetPackage.Metadata.Description,
Author: nugetPackage.Metadata.Author,
License: nugetPackage.Metadata.License,
SourceCodeURL: nugetPackage.Metadata.Repository.URL,
HomepageURL: nugetPackage.Metadata.ProjectURL,
}
return pkgInfo
}
33 changes: 33 additions & 0 deletions internal/backends/dotnet/nuget_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dotnet

import (
"testing"
)

func TestSearchNuget(t *testing.T) {
pkgs := search("Microsoft.Extensions.Logging")

if len(pkgs) < 1 {
t.Error("No results found for Micorosft.Extensions.Logging")
}

for _, pkg := range pkgs {
if pkg.Name == "" {
t.Errorf("pkg %q has no name", pkg)
}
if pkg.Version == "" {
t.Errorf("pkg %q has no version", pkg)
}
}
}

func TestInfoFromNuget(t *testing.T) {
pkg := info("Microsoft.Extensions.Logging")

if pkg.Name == "" {
t.Errorf("pkg %q has no name", pkg)
}
if pkg.Version == "" {
t.Errorf("pkg %q has no version", pkg)
}
}
Loading