diff --git a/.gitignore b/.gitignore index f5930e79..5fdb8fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ test/resolve/testdata/pip/requirements.txt.venv/ test/resolve/testdata/pip/.requirements.txt.debricked.lock internal/cmd/scan/testdata/npm/yarn.lock internal/resolution/pm/gradle/.gradle-init-script.debricked.groovy -test/resolve/testdata/npm/yarn.lock \ No newline at end of file +test/resolve/testdata/npm/yarn.lock +test/resolve/testdata/nuget/packages.lock.json +test/resolve/testdata/nuget/obj diff --git a/build/docker/alpine.Dockerfile b/build/docker/alpine.Dockerfile index 9195ddfc..d25bcd3d 100644 --- a/build/docker/alpine.Dockerfile +++ b/build/docker/alpine.Dockerfile @@ -25,7 +25,8 @@ RUN apk --no-cache --update add \ py3-pip \ go~=1.20 \ nodejs \ - yarn + yarn \ + dotnet7-sdk ENV MAVEN_VERSION 3.9.2 ENV MAVEN_HOME /usr/lib/mvn @@ -40,4 +41,4 @@ ENV GRADLE_HOME /usr/lib/gradle ENV PATH $GRADLE_HOME/gradle-$GRADLE_VERSION/bin:$PATH RUN wget https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip && \ unzip gradle-$GRADLE_VERSION-bin.zip -d $GRADLE_HOME && \ - rm gradle-$GRADLE_VERSION-bin.zip + rm gradle-$GRADLE_VERSION-bin.zip \ No newline at end of file diff --git a/build/docker/debian.Dockerfile b/build/docker/debian.Dockerfile index 41fcd35b..18f6d715 100644 --- a/build/docker/debian.Dockerfile +++ b/build/docker/debian.Dockerfile @@ -62,3 +62,8 @@ RUN apt -y update && apt -y upgrade && apt -y install nodejs && \ apt -y clean && rm -rf /var/lib/apt/lists/* RUN npm install -g npm@latest RUN npm install --global yarn + + +RUN wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb +RUN rm packages-microsoft-prod.deb +RUN apt -y update && apt -y upgrade && apt install -y dotnet-sdk-7.0 \ No newline at end of file diff --git a/internal/file/default_exclusion.go b/internal/file/default_exclusion.go index 1d3e2033..95e2b1fa 100644 --- a/internal/file/default_exclusion.go +++ b/internal/file/default_exclusion.go @@ -7,5 +7,6 @@ func DefaultExclusions() []string { filepath.Join("**", "node_modules", "**"), filepath.Join("**", "vendor", "**"), filepath.Join("**", ".git", "**"), + filepath.Join("**", "obj", "**"), // nuget } } diff --git a/internal/resolution/pm/nuget/cmd_factory.go b/internal/resolution/pm/nuget/cmd_factory.go new file mode 100644 index 00000000..cdb3e665 --- /dev/null +++ b/internal/resolution/pm/nuget/cmd_factory.go @@ -0,0 +1,39 @@ +package nuget + +import ( + "os/exec" + "path/filepath" +) + +type ICmdFactory interface { + MakeInstallCmd(command string, file string) (*exec.Cmd, error) +} + +type IExecPath interface { + LookPath(file string) (string, error) +} + +type ExecPath struct { +} + +func (ExecPath) LookPath(file string) (string, error) { + return exec.LookPath(file) +} + +type CmdFactory struct { + execPath IExecPath +} + +func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, error) { + path, err := cmdf.execPath.LookPath(command) + + fileDir := filepath.Dir(file) + + return &exec.Cmd{ + Path: path, + Args: []string{command, "restore", + "--use-lock-file", + }, + Dir: fileDir, + }, err +} diff --git a/internal/resolution/pm/nuget/cmd_factory_test.go b/internal/resolution/pm/nuget/cmd_factory_test.go new file mode 100644 index 00000000..8bb78d4f --- /dev/null +++ b/internal/resolution/pm/nuget/cmd_factory_test.go @@ -0,0 +1,19 @@ +package nuget + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMakeInstallCmd(t *testing.T) { + nugetCommand := "dotnet" + cmd, err := CmdFactory{ + execPath: ExecPath{}, + }.MakeInstallCmd(nugetCommand, "file") + assert.NoError(t, err) + assert.NotNil(t, cmd) + args := cmd.Args + assert.Contains(t, args, "dotnet") + assert.Contains(t, args, "restore") +} diff --git a/internal/resolution/pm/nuget/job.go b/internal/resolution/pm/nuget/job.go new file mode 100644 index 00000000..479d8b4b --- /dev/null +++ b/internal/resolution/pm/nuget/job.go @@ -0,0 +1,64 @@ +package nuget + +import ( + "fmt" + + "github.com/debricked/cli/internal/resolution/job" +) + +const ( + nuget = "dotnet" +) + +type Job struct { + job.BaseJob + install bool + nugetCommand string + cmdFactory ICmdFactory +} + +func NewJob( + file string, + install bool, + cmdFactory ICmdFactory, +) *Job { + return &Job{ + BaseJob: job.NewBaseJob(file), + install: install, + cmdFactory: cmdFactory, + } +} + +func (j *Job) Install() bool { + return j.install +} + +func (j *Job) Run() { + if j.install { + + j.SendStatus("installing dependencies") + output, err := j.runInstallCmd() + if err != nil { + j.Errors().Critical(fmt.Errorf("%s\n%s", output, err)) + + return + } + } + +} + +func (j *Job) runInstallCmd() ([]byte, error) { + + j.nugetCommand = nuget + installCmd, err := j.cmdFactory.MakeInstallCmd(j.nugetCommand, j.GetFile()) + if err != nil { + return nil, err + } + + installCmdOutput, err := installCmd.Output() + if err != nil { + return installCmdOutput, j.GetExitError(err) + } + + return installCmdOutput, nil +} diff --git a/internal/resolution/pm/nuget/job_test.go b/internal/resolution/pm/nuget/job_test.go new file mode 100644 index 00000000..3ea4c409 --- /dev/null +++ b/internal/resolution/pm/nuget/job_test.go @@ -0,0 +1,64 @@ +package nuget + +import ( + "errors" + "testing" + + jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" + "github.com/debricked/cli/internal/resolution/pm/nuget/testdata" + "github.com/stretchr/testify/assert" +) + +const ( + badName = "bad-name" +) + +func TestNewJob(t *testing.T) { + j := NewJob("file", false, CmdFactory{ + execPath: ExecPath{}, + }) + assert.Equal(t, "file", j.GetFile()) + assert.False(t, j.Errors().HasError()) +} + +func TestRunInstall(t *testing.T) { + cmdFactoryMock := testdata.NewEchoCmdFactory() + j := NewJob("file", false, cmdFactoryMock) + + _, err := j.runInstallCmd() + assert.NoError(t, err) + + assert.False(t, j.Errors().HasError()) +} + +func TestInstall(t *testing.T) { + j := Job{install: true} + assert.Equal(t, true, j.Install()) + + j = Job{install: false} + assert.Equal(t, false, j.Install()) +} + +func TestRunInstallCmdErr(t *testing.T) { + cmdErr := errors.New("cmd-error") + cmdErrGt := errors.New("\ncmd-error") + cmdFactoryMock := testdata.NewEchoCmdFactory() + cmdFactoryMock.MakeInstallErr = cmdErr + j := NewJob("file", true, cmdFactoryMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + assert.Equal(t, j.Errors().GetAll()[0], cmdErrGt) +} + +func TestRunInstallCmdOutputErr(t *testing.T) { + cmdMock := testdata.NewEchoCmdFactory() + cmdMock.InstallCmdName = badName + j := NewJob("file", true, cmdMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + jobTestdata.AssertPathErr(t, j.Errors()) +} diff --git a/internal/resolution/pm/nuget/pm.go b/internal/resolution/pm/nuget/pm.go new file mode 100644 index 00000000..21d43d60 --- /dev/null +++ b/internal/resolution/pm/nuget/pm.go @@ -0,0 +1,23 @@ +package nuget + +const Name = "nuget" + +type Pm struct { + name string +} + +func NewPm() Pm { + return Pm{ + name: Name, + } +} + +func (pm Pm) Name() string { + return pm.name +} + +func (Pm) Manifests() []string { + return []string{ + `\.csproj$`, + } +} diff --git a/internal/resolution/pm/nuget/pm_test.go b/internal/resolution/pm/nuget/pm_test.go new file mode 100644 index 00000000..4e53bc79 --- /dev/null +++ b/internal/resolution/pm/nuget/pm_test.go @@ -0,0 +1,44 @@ +package nuget + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewPm(t *testing.T) { + pm := NewPm() + assert.Equal(t, Name, pm.name) +} + +func TestName(t *testing.T) { + pm := NewPm() + assert.Equal(t, Name, pm.Name()) +} + +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.NoError(t, err) + + cases := map[string]bool{ + "test.csproj": true, + "sample3.csproj": true, + ".csproj": true, + "test.csproj.user": false, + "test.csproj.nuget": false, + "test.csproj.nuget.props": false, + "package.json.lock": false, + } + for file, isMatch := range cases { + t.Run(file, func(t *testing.T) { + matched, _ := regexp.MatchString(manifest, file) + assert.Equal(t, isMatch, matched) + }) + } +} diff --git a/internal/resolution/pm/nuget/strategy.go b/internal/resolution/pm/nuget/strategy.go new file mode 100644 index 00000000..355c1ccd --- /dev/null +++ b/internal/resolution/pm/nuget/strategy.go @@ -0,0 +1,29 @@ +package nuget + +import ( + "github.com/debricked/cli/internal/resolution/job" +) + +type Strategy struct { + files []string +} + +func (s Strategy) Invoke() ([]job.IJob, error) { + var jobs []job.IJob + for _, file := range s.files { + jobs = append(jobs, NewJob( + file, + true, + CmdFactory{ + execPath: ExecPath{}, + }, + ), + ) + } + + return jobs, nil +} + +func NewStrategy(files []string) Strategy { + return Strategy{files} +} diff --git a/internal/resolution/pm/nuget/strategy_test.go b/internal/resolution/pm/nuget/strategy_test.go new file mode 100644 index 00000000..8d0684fd --- /dev/null +++ b/internal/resolution/pm/nuget/strategy_test.go @@ -0,0 +1,43 @@ +package nuget + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewStrategy(t *testing.T) { + s := NewStrategy(nil) + assert.NotNil(t, s) + assert.Len(t, s.files, 0) + + s = NewStrategy([]string{}) + assert.NotNil(t, s) + assert.Len(t, s.files, 0) + + s = NewStrategy([]string{"file"}) + assert.NotNil(t, s) + assert.Len(t, s.files, 1) + + s = NewStrategy([]string{"file-1", "file-2"}) + assert.NotNil(t, s) + assert.Len(t, s.files, 2) +} + +func TestInvokeNoFiles(t *testing.T) { + s := NewStrategy([]string{}) + jobs, _ := s.Invoke() + assert.Empty(t, jobs) +} + +func TestInvokeOneFile(t *testing.T) { + s := NewStrategy([]string{"file"}) + jobs, _ := s.Invoke() + assert.Len(t, jobs, 1) +} + +func TestInvokeManyFiles(t *testing.T) { + s := NewStrategy([]string{"file-1", "file-2"}) + jobs, _ := s.Invoke() + assert.Len(t, jobs, 2) +} diff --git a/internal/resolution/pm/nuget/testdata/cmd_factory_mock.go b/internal/resolution/pm/nuget/testdata/cmd_factory_mock.go new file mode 100644 index 00000000..093ba6b2 --- /dev/null +++ b/internal/resolution/pm/nuget/testdata/cmd_factory_mock.go @@ -0,0 +1,20 @@ +package testdata + +import ( + "os/exec" +) + +type CmdFactoryMock struct { + InstallCmdName string + MakeInstallErr error +} + +func NewEchoCmdFactory() CmdFactoryMock { + return CmdFactoryMock{ + InstallCmdName: "echo", + } +} + +func (f CmdFactoryMock) MakeInstallCmd(command string, file string) (*exec.Cmd, error) { + return exec.Command(f.InstallCmdName), f.MakeInstallErr +} diff --git a/internal/resolution/pm/pm.go b/internal/resolution/pm/pm.go index 2fdb5a73..a0860ebd 100644 --- a/internal/resolution/pm/pm.go +++ b/internal/resolution/pm/pm.go @@ -4,8 +4,9 @@ import ( "github.com/debricked/cli/internal/resolution/pm/gomod" "github.com/debricked/cli/internal/resolution/pm/gradle" "github.com/debricked/cli/internal/resolution/pm/maven" + "github.com/debricked/cli/internal/resolution/pm/nuget" "github.com/debricked/cli/internal/resolution/pm/pip" - npm "github.com/debricked/cli/internal/resolution/pm/yarn" + "github.com/debricked/cli/internal/resolution/pm/yarn" ) type IPm interface { @@ -19,6 +20,7 @@ func Pms() []IPm { gradle.NewPm(), gomod.NewPm(), pip.NewPm(), - npm.NewPm(), + yarn.NewPm(), + nuget.NewPm(), } } diff --git a/internal/resolution/strategy/strategy_factory.go b/internal/resolution/strategy/strategy_factory.go index e2dbf6d2..40c95926 100644 --- a/internal/resolution/strategy/strategy_factory.go +++ b/internal/resolution/strategy/strategy_factory.go @@ -7,6 +7,7 @@ import ( "github.com/debricked/cli/internal/resolution/pm/gomod" "github.com/debricked/cli/internal/resolution/pm/gradle" "github.com/debricked/cli/internal/resolution/pm/maven" + "github.com/debricked/cli/internal/resolution/pm/nuget" "github.com/debricked/cli/internal/resolution/pm/pip" "github.com/debricked/cli/internal/resolution/pm/yarn" ) @@ -34,6 +35,8 @@ func (sf Factory) Make(pmFileBatch file.IBatch, paths []string) (IStrategy, erro return pip.NewStrategy(pmFileBatch.Files()), nil case yarn.Name: return yarn.NewStrategy(pmFileBatch.Files()), nil + case nuget.Name: + return nuget.NewStrategy(pmFileBatch.Files()), nil default: return nil, fmt.Errorf("failed to make strategy from %s", name) } diff --git a/internal/resolution/strategy/strategy_factory_test.go b/internal/resolution/strategy/strategy_factory_test.go index b0987a19..1eeca37f 100644 --- a/internal/resolution/strategy/strategy_factory_test.go +++ b/internal/resolution/strategy/strategy_factory_test.go @@ -7,8 +7,10 @@ import ( "github.com/debricked/cli/internal/resolution/pm/gomod" "github.com/debricked/cli/internal/resolution/pm/gradle" "github.com/debricked/cli/internal/resolution/pm/maven" + "github.com/debricked/cli/internal/resolution/pm/nuget" "github.com/debricked/cli/internal/resolution/pm/pip" "github.com/debricked/cli/internal/resolution/pm/testdata" + "github.com/debricked/cli/internal/resolution/pm/yarn" "github.com/stretchr/testify/assert" ) @@ -31,6 +33,8 @@ func TestMake(t *testing.T) { gradle.Name: gradle.NewStrategy(nil, nil), gomod.Name: gomod.NewStrategy(nil), pip.Name: pip.NewStrategy(nil), + yarn.Name: yarn.NewStrategy(nil), + nuget.Name: nuget.NewStrategy(nil), } f := NewStrategyFactory() var batch file.IBatch diff --git a/test/resolve/resolver_test.go b/test/resolve/resolver_test.go index f500314f..f5ae1931 100644 --- a/test/resolve/resolver_test.go +++ b/test/resolve/resolver_test.go @@ -29,6 +29,12 @@ func TestResolves(t *testing.T) { lockFileName: ".requirements.txt.debricked.lock", expectedFile: "testdata/pip/expected.lock", }, + { + name: "basic .csproj", + manifestFile: "testdata/nuget/basic.csproj", + lockFileName: "packages.lock.json", + expectedFile: "testdata/nuget/packages-expected.lock.json", + }, } for _, c := range cases { diff --git a/test/resolve/testdata/nuget/basic.csproj b/test/resolve/testdata/nuget/basic.csproj new file mode 100644 index 00000000..14e39507 --- /dev/null +++ b/test/resolve/testdata/nuget/basic.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/test/resolve/testdata/nuget/packages-expected.lock.json b/test/resolve/testdata/nuget/packages-expected.lock.json new file mode 100644 index 00000000..00540ac0 --- /dev/null +++ b/test/resolve/testdata/nuget/packages-expected.lock.json @@ -0,0 +1,156 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "Microsoft.ML": { + "type": "Direct", + "requested": "[2.0.0, )", + "resolved": "2.0.0", + "contentHash": "bUU+TwpgEAQamvst0XJ7xnONHPGfbJXqorvAJ1onv5g6jF+Yq10oLWPhn74SJsX/WjYbQSkKqr/g7K75oIDpaQ==", + "dependencies": { + "Microsoft.ML.CpuMath": "2.0.0", + "Microsoft.ML.DataView": "2.0.0", + "Newtonsoft.Json": "13.0.1", + "System.CodeDom": "4.5.0", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Threading.Channels": "4.7.1" + } + }, + "Microsoft.ML.CpuMath": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "O2hIWMPqGwz4OS+Sf4fZ3fjIypRmh49GAMBSDCCAfhSY+ZmXqcYDNGS++f322tDokWgXGErp61MgORfU/5RA6g==" + }, + "Microsoft.ML.DataView": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Czgi6Oh1NNOHKR+TQuxWmhNHZsoN3QbZ8GqMvYEXcxVF1lZOQmk9gM3qPmc3vfC8feheUjmyKERYz2fVC0GHaQ==", + "dependencies": { + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "gqpR1EeXOuzNQWL7rOzmtdIz3CaXVjSQCiaGOs2ivjPwynKSJYm39X81fdlp7WuojZs/Z5t1k5ni7HtKQurhjw==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "6akRtHK/wab3246t4p5v3HQrtQk8LboOt5T4dtpNgsp3zvDeM4/Gx8V12t0h+c/W9/enUrilk8n6EQqdQorZAA==" + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + } + } + } +} \ No newline at end of file