Skip to content

Commit

Permalink
Add plugin for Swift Package Manager
Browse files Browse the repository at this point in the history
Signed-off-by: Mattt Zmuda <[email protected]>
  • Loading branch information
mattt committed Jul 26, 2021
1 parent 82c5817 commit 165d1ea
Show file tree
Hide file tree
Showing 17 changed files with 561 additions and 1 deletion.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@ test: prepare-test
prepare-test:
@cd pkg/modules/npm/test && npm install
@cd pkg/modules/yarn/test && yarn install
@cd pkg/modules/swift/test && swift build
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* PIP (Python)
* Pipenv (Python)
* Gems (Ruby)
* Swift Package Manager (Swift)

## Installation

Expand Down
2 changes: 1 addition & 1 deletion check-headers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Exits with a 1 if one or more source files are missing a license header

# These are the file patterns we should exclude - these are typically transient files not checked into source control
exclude_pattern='LICENSES/|vendor|node_modules|.venv|.pytest_cache|.idea|version.txt'
exclude_pattern='LICENSES/|vendor|node_modules|pkg/modules/swift/test|.venv|.pytest_cache|.idea|version.txt'

files=()
echo "Scanning source code..."
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ require (
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.6.1
github.com/vifraa/gopom v0.1.0
golang.org/x/mod v0.4.2
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand All @@ -278,6 +279,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -345,7 +348,9 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.7.0 h1:Hdks0L0hgznZLG9nzXb8vZ0rRvqNvAcgAp84y7Mwkgw=
gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM=
Expand Down
3 changes: 3 additions & 0 deletions pkg/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package modules

import (
"errors"

"github.com/spdx/spdx-sbom-generator/pkg/modules/javagradle"

log "github.com/sirupsen/logrus"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/spdx/spdx-sbom-generator/pkg/modules/npm"
"github.com/spdx/spdx-sbom-generator/pkg/modules/nuget"
"github.com/spdx/spdx-sbom-generator/pkg/modules/pip"
"github.com/spdx/spdx-sbom-generator/pkg/modules/swift"
"github.com/spdx/spdx-sbom-generator/pkg/modules/yarn"
)

Expand All @@ -40,6 +42,7 @@ func init() {
nuget.New(),
yarn.New(),
pip.New(),
swift.New(),
)
}

Expand Down
9 changes: 9 additions & 0 deletions pkg/modules/swift/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: Apache-2.0

package swift

import (
"errors"
)

var errDependenciesNotFound error = errors.New("unable to generate SPDX file, no modules or vendors found. Please install them before running spdx-sbom-generator, e.g.: `swift build`")
165 changes: 165 additions & 0 deletions pkg/modules/swift/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: Apache-2.0

package swift

import (
"bytes"
"encoding/json"
"os/exec"
"path/filepath"

"github.com/spdx/spdx-sbom-generator/pkg/helper"
"github.com/spdx/spdx-sbom-generator/pkg/models"
)

type pkg struct {
metadata models.PluginMetadata
}

const (
ManifestFile string = "Package.swift"
BuildDirectory string = ".build"
)

// New creates a new Swift package instance
func New() *pkg {
return &pkg{
metadata: models.PluginMetadata{
Name: "Swift Package Manager",
Slug: "swift",
Manifest: []string{ManifestFile},
ModulePath: []string{BuildDirectory},
},
}
}

// GetVersion returns Swift language version
func (m *pkg) GetVersion() (string, error) {
cmd := exec.Command("swift", "--version")
output, err := cmd.Output()
if err != nil {
return "", err
}

version := string(output)

return version, nil
}

// GetMetadata returns root package information base on path given
func (m *pkg) GetMetadata() models.PluginMetadata {
return m.metadata
}

// SetRootModule sets root package information base on path given
func (m *pkg) SetRootModule(path string) error {
return nil
}

// GetRootModule returns root package information base on path given
func (m *pkg) GetRootModule(path string) (*models.Module, error) {
cmd := exec.Command("swift", "package", "describe", "--type", "json")
cmd.Dir = path
output, err := cmd.Output()
if err != nil {
return nil, err
}

var description SwiftPackageDescription
if err := json.NewDecoder(bytes.NewReader(output)).Decode(&description); err != nil {
return nil, err
}

mod := description.Module()

return mod, nil
}

// ListUsedModules fetches and lists
// all packages required by the project
// in the given project directory,
// this is a plain list of all used modules
// (no nested or tree view)
func (m *pkg) ListUsedModules(path string) ([]models.Module, error) {
cmd := exec.Command("swift", "package", "show-dependencies", "--disable-automatic-resolution", "--format", "json")
cmd.Dir = path
output, err := cmd.Output()
if err != nil {
return nil, err
}

var root SwiftPackageDependency
if err := json.NewDecoder(bytes.NewReader(output)).Decode(&root); err != nil {
return nil, err
}

var dependencies []SwiftPackageDependency

var recurse func(SwiftPackageDependency)
recurse = func(dep SwiftPackageDependency) {
for _, nested := range dep.Dependencies {
dependencies = append(dependencies, nested)
recurse(nested)
}
}
recurse(root)

var collection []models.Module
for _, dep := range dependencies {
mod := dep.Module()
collection = append(collection, *mod)
}

return collection, nil
}

// ListModulesWithDeps fetches and lists all packages
// (root and direct dependencies)
// required by the project in the given project directory (side-by-side),
// this is a one level only list of all used modules,
// and each with its direct dependency only
// (similar output to ListUsedModules but with direct dependency only)
func (m *pkg) ListModulesWithDeps(path string) ([]models.Module, error) {
var collection []models.Module

mod, err := m.GetRootModule(path)
if err != nil {
return nil, err
}
collection = append(collection, *mod)

cmd := exec.Command("swift", "package", "show-dependencies", "--disable-automatic-resolution", "--format", "json")
cmd.Dir = path
output, err := cmd.Output()
if err != nil {
return nil, err
}

var root SwiftPackageDependency
if err := json.NewDecoder(bytes.NewReader(output)).Decode(&root); err != nil {
return nil, err
}

for _, dep := range root.Dependencies {
mod := dep.Module()
collection = append(collection, *mod)
}

return collection, nil
}

// IsValid checks if the project dependency file provided in the contract exists
func (m *pkg) IsValid(path string) bool {
return helper.Exists(filepath.Join(path, ManifestFile))
}

// HasModulesInstalled checks whether
// the current project (based on given path)
// has the dependent packages installed
func (m *pkg) HasModulesInstalled(path string) error {
if helper.Exists(filepath.Join(path, BuildDirectory)) {
return nil
}

return errDependenciesNotFound
}
126 changes: 126 additions & 0 deletions pkg/modules/swift/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: Apache-2.0

package swift

import (
"fmt"
"os/exec"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSwift(t *testing.T) {
t.Run("test is valid", TestIsValid)
t.Run("test has modules installed", TestHasModulesInstalled)
t.Run("test get root module", TestGetRootModule)
t.Run("test list used modules", TestListUsedModules)
t.Run("test list modules with dependencies", TestListModulesWithDeps)
}

func TestIsValid(t *testing.T) {
n := New()
path := fmt.Sprintf("%s/test", getPath())

valid := n.IsValid(path)
invalid := n.IsValid(getPath())

// Assert
assert.Equal(t, true, valid)
assert.Equal(t, false, invalid)
}

func TestHasModulesInstalled(t *testing.T) {
n := New()
path := fmt.Sprintf("%s/test", getPath())

installed := n.HasModulesInstalled(path)
assert.NoError(t, installed)
uninstalled := n.HasModulesInstalled(getPath())
assert.Error(t, uninstalled)
}

func TestGetRootModule(t *testing.T) {
n := New()
path := fmt.Sprintf("%s/test", getPath())
mod, err := n.GetRootModule(path)

assert.NoError(t, err)
assert.Equal(t, "Example", mod.Name)
assert.Equal(t, "MIT", mod.LicenseConcluded)
}

func TestListUsedModules(t *testing.T) {
n := New()
path := fmt.Sprintf("%s/test", getPath())
mods, err := n.ListUsedModules(path)

assert.NoError(t, err)

count := 0
for _, mod := range mods {
if mod.Name == "DeckOfPlayingCards" {
assert.Equal(t, "3.0.4", mod.Version)
assert.Equal(t, "https://github.com/apple/example-package-deckofplayingcards", mod.PackageURL)
assert.Equal(t, "git+https://github.com/apple/example-package-deckofplayingcards.git", mod.PackageDownloadLocation)
count++
continue
}

if mod.Name == "FisherYates" {
assert.Equal(t, "2.0.6", mod.Version)
assert.Equal(t, "https://github.com/apple/example-package-fisheryates", mod.PackageURL)
assert.Equal(t, "git+https://github.com/apple/example-package-fisheryates.git", mod.PackageDownloadLocation)
count++
continue
}

if mod.Name == "PlayingCard" {
assert.Equal(t, "3.0.5", mod.Version)
assert.Equal(t, "https://github.com/apple/example-package-playingcard", mod.PackageURL)
assert.Equal(t, "git+https://github.com/apple/example-package-playingcard.git", mod.PackageDownloadLocation)
count++
continue
}
}

assert.Equal(t, 3, count)
}

func TestListModulesWithDeps(t *testing.T) {
n := New()
path := fmt.Sprintf("%s/test", getPath())
mods, err := n.ListModulesWithDeps(path)

assert.NoError(t, err)

count := 0
for _, mod := range mods {
if mod.Name == "Example" {
count++
continue
}

if mod.Name == "DeckOfPlayingCards" {
assert.Equal(t, "3.0.4", mod.Version)
assert.Equal(t, "https://github.com/apple/example-package-deckofplayingcards", mod.PackageURL)
assert.Equal(t, "git+https://github.com/apple/example-package-deckofplayingcards.git", mod.PackageDownloadLocation)
count++
continue
}
}

assert.Equal(t, 2, count)
}

func getPath() string {
cmd := exec.Command("pwd")
output, err := cmd.Output()
if err != nil {
return ""
}
path := strings.TrimSuffix(string(output), "\n")

return path
}
Loading

0 comments on commit 165d1ea

Please sign in to comment.