Skip to content

Commit

Permalink
initial support for packages.config resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
emilwareus committed Sep 8, 2023
1 parent 2890ec0 commit 1ddccfb
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 7 deletions.
144 changes: 144 additions & 0 deletions internal/resolution/pm/nuget/cmd_factory.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package nuget

import (
"bytes"
"encoding/xml"
"html/template"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)

type ICmdFactory interface {
Expand All @@ -25,8 +32,27 @@ type CmdFactory struct {
}

func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, error) {

// If the file is a packages.config file, convert it to a .csproj file
// check regex with PackagesConfigRegex
packageConfig, err := regexp.Compile(PackagesConfigRegex)
if err != nil {
return nil, err
}

if packageConfig.MatchString(file) {
file, err = convertPackagesConfigToCsproj(file)
if err != nil {
return nil, err
}
}

path, err := cmdf.execPath.LookPath(command)

if err != nil {
return nil, err
}

fileDir := filepath.Dir(file)

return &exec.Cmd{
Expand All @@ -37,3 +63,121 @@ func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, e
Dir: fileDir,
}, err
}

type Packages struct {
Packages []Package `xml:"package"`
}

type Package struct {
ID string `xml:"id,attr"`
Version string `xml:"version,attr"`
TargetFramework string `xml:"targetFramework,attr"`
}

// convertPackagesConfigtoCsproj converts a packages.config file to a .csproj file
// this is to enable the use of the dotnet restore command
// that enables debricked to parse out transitive dependencies.
// This may add some additional framework dependencies that will not show up if
// we only scan the packages.config file.
func convertPackagesConfigToCsproj(filePath string) (string, error) {
packages, err := parsePackagesConfig(filePath)
if err != nil {
return "", err
}

targetFrameworksStr := collectUniqueTargetFrameworks(packages.Packages)
csprojContent, err := createCsprojContent(targetFrameworksStr, packages.Packages)
if err != nil {
return "", err
}

newFilename := filePath + ".csproj"
err = writeContentToCsprojFile(newFilename, csprojContent)
if err != nil {
return "", err
}

return newFilename, nil
}

func parsePackagesConfig(filePath string) (*Packages, error) {
xmlFile, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer xmlFile.Close()

byteValue, err := io.ReadAll(xmlFile)
if err != nil {
return nil, err
}

var packages Packages
err = xml.Unmarshal(byteValue, &packages)
if err != nil {
return nil, err
}

return &packages, nil
}

func collectUniqueTargetFrameworks(packages []Package) string {
uniqueTargetFrameworks := make(map[string]struct{})
for _, pkg := range packages {
uniqueTargetFrameworks[pkg.TargetFramework] = struct{}{}
}

var targetFrameworks []string
for framework := range uniqueTargetFrameworks {
if framework != "" {
targetFrameworks = append(targetFrameworks, framework)
}
}

return strings.Join(targetFrameworks, ";")
}

func createCsprojContent(targetFrameworksStr string, packages []Package) (string, error) {
tmpl := `
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>{{.TargetFrameworks}}</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
{{- range .Packages}}
<PackageReference Include="{{.ID}}" Version="{{.Version}}" />
{{- end}}
</ItemGroup>
</Project>
`
tmplParsed, err := template.New("csproj").Parse(tmpl)
if err != nil {
return "", err
}

var tpl bytes.Buffer
err = tmplParsed.Execute(&tpl, map[string]interface{}{
"TargetFrameworks": targetFrameworksStr,
"Packages": packages,
})
if err != nil {
return "", err
}

return tpl.String(), nil
}

func writeContentToCsprojFile(newFilename string, content string) error {
csprojFile, err := os.Create(newFilename)
if err != nil {
return err
}
defer csprojFile.Close()

_, err = csprojFile.WriteString(content)
if err != nil {
return err
}

return nil
}
107 changes: 107 additions & 0 deletions internal/resolution/pm/nuget/cmd_factory_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nuget

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -17,3 +18,109 @@ func TestMakeInstallCmd(t *testing.T) {
assert.Contains(t, args, "dotnet")
assert.Contains(t, args, "restore")
}

func TestMakeInstallCmdPackagsConfig(t *testing.T) {
nugetCommand := "dotnet"
cmd, err := CmdFactory{
execPath: ExecPath{},
}.MakeInstallCmd(nugetCommand, "testdata/valid/packages.config")
assert.NoError(t, err)
assert.NotNil(t, cmd)
args := cmd.Args
assert.Contains(t, args, "dotnet")
assert.Contains(t, args, "restore")

// Cleanup: Remove the created .csproj file
if err := os.Remove("testdata/valid/packages.config.csproj"); err != nil {
t.Fatalf("Failed to remove test file: %v", err)
}
}

func TestParsePackagesConfig(t *testing.T) {
tests := []struct {
filePath string
wantError bool
}{
{"testdata/valid/packages.config", false},
{"testdata/invalid/packages.config", true},
}

for _, tt := range tests {
_, err := parsePackagesConfig(tt.filePath)
if (err != nil) != tt.wantError {
t.Errorf("parsePackagesConfig(%q) = %v, want error: %v", tt.filePath, err, tt.wantError)
}
}
}

func TestCollectUniqueTargetFrameworks(t *testing.T) {
packages := []Package{
{TargetFramework: "net45"},
{TargetFramework: "net46"},
{TargetFramework: "net45"},
}
got := collectUniqueTargetFrameworks(packages)
want := "net45;net46"
if got != want {
t.Errorf("collectUniqueTargetFrameworks() = %v, want %v", got, want)
}
}

func TestCreateCsprojContent(t *testing.T) {
packages := []Package{
{ID: "Test.Package.1", Version: "1.0.0", TargetFramework: "net45"},
{ID: "Test.Package.2", Version: "2.0.0", TargetFramework: "net46"},
}
targetFrameworksStr := "net45;net46"

got, err := createCsprojContent(targetFrameworksStr, packages)
if err != nil {
t.Fatalf("createCsprojContent() failed: %v", err)
}

// We're just checking if the function returns a non-empty string
// For a more rigorous test, we'd check the exact content of the string
if got == "" {
t.Errorf("createCsprojContent() returned an empty string")
}
}

func TestWriteContentToCsprojFile(t *testing.T) {
newFilename := "testdata/test_output.csproj"
content := "<Project></Project>"

if err := writeContentToCsprojFile(newFilename, content); err != nil {
t.Fatalf("writeContentToCsprojFile() failed: %v", err)
}

if _, err := os.Stat(newFilename); os.IsNotExist(err) {
t.Fatalf("writeContentToCsprojFile() did not create file")
}

// Cleanup: Remove the created file
if err := os.Remove(newFilename); err != nil {
t.Fatalf("Failed to remove test file: %v", err)
}
}

func TestConvertPackagesConfigToCsproj(t *testing.T) {
tests := []struct {
filePath string
wantError bool
}{
{"testdata/valid/packages.config", false},
{"testdata/invalid/packages.config", true},
}

for _, tt := range tests {
_, err := convertPackagesConfigToCsproj(tt.filePath)
if (err != nil) != tt.wantError {
t.Errorf("convertPackagesConfigToCsproj(%q) = %v, want error: %v", tt.filePath, err, tt.wantError)
}
}

// Cleanup: Remove the created .csproj file
if err := os.Remove("testdata/valid/packages.config.csproj"); err != nil {
t.Fatalf("Failed to remove test file: %v", err)
}
}
5 changes: 4 additions & 1 deletion internal/resolution/pm/nuget/pm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package nuget

const Name = "nuget"
const CsprojRegex = `\.csproj$`
const PackagesConfigRegex = `packages\.config$`

type Pm struct {
name string
Expand All @@ -18,6 +20,7 @@ func (pm Pm) Name() string {

func (Pm) Manifests() []string {
return []string{
`\.csproj$`,
CsprojRegex,
PackagesConfigRegex,
}
}
22 changes: 16 additions & 6 deletions internal/resolution/pm/nuget/pm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ func TestName(t *testing.T) {
func TestManifests(t *testing.T) {
pm := Pm{}
manifests := pm.Manifests()
assert.Len(t, manifests, 1)
manifest := manifests[0]
assert.Equal(t, `\.csproj$`, manifest)
_, err := regexp.Compile(manifest)
assert.Len(t, manifests, 2)
manifestCs := manifests[0]
assert.Equal(t, `\.csproj$`, manifestCs)
_, err := regexp.Compile(manifestCs)
assert.NoError(t, err)

manifestPc := manifests[1]
assert.Equal(t, `packages\.config$`, manifestPc)
_, err = regexp.Compile(manifestPc)
assert.NoError(t, err)

cases := map[string]bool{
Expand All @@ -34,11 +39,16 @@ func TestManifests(t *testing.T) {
"test.csproj.nuget": false,
"test.csproj.nuget.props": false,
"package.json.lock": false,
"packages.config": true,
}
for file, isMatch := range cases {
t.Run(file, func(t *testing.T) {
matched, _ := regexp.MatchString(manifest, file)
assert.Equal(t, isMatch, matched)

matchedCs, _ := regexp.MatchString(manifestCs, file)
matchedPc, _ := regexp.MatchString(manifestPc, file)
matched := matchedCs || matchedPc

assert.Equal(t, isMatch, matched, "file: %s", file)
})
}
}
4 changes: 4 additions & 0 deletions internal/resolution/pm/nuget/testdata/invalid/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- This file intentionally contains invalid XML -->
<packages>
<package id="Test.Package.1" version="1.0.0" targetFramework="net45">
</packages>
5 changes: 5 additions & 0 deletions internal/resolution/pm/nuget/testdata/valid/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Test.Package.1" version="1.0.0" targetFramework="net45" />
<package id="Test.Package.2" version="2.0.0" targetFramework="net46" />
</packages>

0 comments on commit 1ddccfb

Please sign in to comment.