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

Add support for Swift / Swift Package Manager #363

Merged
merged 14 commits into from
May 27, 2021
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,33 @@ jobs:
- name: Run tests
run: script/test pipenv

swift:
runs-on: ubuntu-latest
strategy:
matrix:
swift: [ "5.4", "5.3" ]
steps:
- uses: actions/checkout@v2
- name: Setup Swift
uses: fwal/setup-swift@v1
with:
swift-version: ${{ matrix.swift }}
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
- run: bundle lock
- uses: actions/cache@v1
with:
path: vendor/gems
key: ${{ runner.os }}-gem-2.6.x-${{ hashFiles('**/Gemfile.lock') }}
- name: Bootstrap
run: script/bootstrap
- name: Set up fixtures
run: script/source-setup/swift
- name: Run tests
run: script/test swift

yarn:
runs-on: ubuntu-latest
strategy:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Dependencies will be automatically detected for all of the following sources by
1. [NuGet](./docs/sources/nuget.md)
1. [Pip](./docs/sources/pip.md)
1. [Pipenv](./docs/sources/pipenv.md)
1. [Swift](./docs/sources/swift.md)
1. [Yarn](./docs/sources/yarn.md)

You can disable any of them in the configuration file:
Expand Down
4 changes: 4 additions & 0 deletions docs/sources/swift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Swift

The Swift source uses `swift package` subcommands
to enumerate dependencies and properties.
1 change: 1 addition & 0 deletions lib/licensed/sources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Sources
require "licensed/sources/nuget"
require "licensed/sources/pip"
require "licensed/sources/pipenv"
require "licensed/sources/swift"
require "licensed/sources/gradle"
require "licensed/sources/mix"
require "licensed/sources/yarn"
Expand Down
64 changes: 64 additions & 0 deletions lib/licensed/sources/swift.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true
require "json"
require "pathname"
require "uri"

module Licensed
module Sources
class Swift < Source
def enabled?
return unless Licensed::Shell.tool_available?("swift") && swift_package?
File.exist?(package_resolved_file_path)
end

def enumerate_dependencies
pins.map { |pin|
name = pin["package"]
version = pin.dig("state", "version")
path = nil
errors = []

begin
path = dependency_path_for_url(pin["repositoryURL"])
rescue => e
errors << e
end

Dependency.new(
name: name,
path: path,
version: version,
errors: errors
)
}
end

private

def pins
return @pins if defined?(@pins)

@pins = begin
json = JSON.parse(File.read(package_resolved_file_path))
json.dig("object", "pins")
rescue => e
message = "Licensed was unable to read the Package.resolved file. Error: #{e.message}"
raise Licensed::Sources::Source::Error, message
end
end

def dependency_path_for_url(url)
last_path_component = URI(url).path.split("/").last.sub(/\.git$/, "")
File.join(config.pwd, ".build", "checkouts", last_path_component)
end

def package_resolved_file_path
File.join(config.pwd, "Package.resolved")
end

def swift_package?
Licensed::Shell.success?("swift", "package", "describe")
end
end
end
end
22 changes: 22 additions & 0 deletions script/source-setup/swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
set -e

if [ -z "$(which swift)" ]; then
echo "A local swift installation is required for swift development." >&2
exit 127
fi

swift --version

BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd $BASE_PATH/test/fixtures/swift

if [ "$1" == "-f" ]; then
find . -not -regex "\.*" \
-and -not -path "*/Package.swift" \
-and -not -path "*/Sources*" \
-and -not -path "*/Tests*" \
-print0 | xargs -0 rm -rf
fi

swift package resolve
5 changes: 5 additions & 0 deletions test/fixtures/command/swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
expected_dependency: DeckOfPlayingCards
source_path: test/fixtures/swift
cache_path: test/fixtures/swift/.licenses
sources:
swift: true
34 changes: 34 additions & 0 deletions test/fixtures/swift/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"object": {
"pins": [
{
"package": "DeckOfPlayingCards",
"repositoryURL": "https://github.com/apple/example-package-deckofplayingcards.git",
"state": {
"branch": null,
"revision": "2c0e5ac3e10216151fc78ac1ec6bd9c2c0111a3a",
"version": "3.0.4"
}
},
{
"package": "FisherYates",
"repositoryURL": "https://github.com/apple/example-package-fisheryates.git",
"state": {
"branch": null,
"revision": "e729f197bbc3831b9a3005fa71ad6f38c1e7e17e",
"version": "2.0.6"
}
},
{
"package": "PlayingCard",
"repositoryURL": "https://github.com/apple/example-package-playingcard.git",
"state": {
"branch": null,
"revision": "39ddabb01e8102ab548a8c6bb3eb20b15f3b4fbc",
"version": "3.0.5"
}
}
]
},
"version": 1
}
31 changes: 31 additions & 0 deletions test/fixtures/swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Fixtures",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Fixtures",
targets: ["Fixtures"]),
],
dependencies: [
.package(name: "DeckOfPlayingCards",
url: "https://github.com/apple/example-package-deckofplayingcards.git",
from: "3.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Fixtures",
dependencies: [
.product(name: "DeckOfPlayingCards", package: "DeckOfPlayingCards")
]),
.testTarget(
name: "FixturesTests",
dependencies: ["Fixtures"]),
]
)
3 changes: 3 additions & 0 deletions test/fixtures/swift/Sources/Fixtures/Fixture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public struct Fixture {
public init() {}
}
8 changes: 8 additions & 0 deletions test/fixtures/swift/Tests/FixturesTests/FixturesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import XCTest
import Fixtures

class FixturesTests: XCTestCase {
func testFixtures() {
XCTAssertNotNil(Fixture())
}
}
85 changes: 85 additions & 0 deletions test/sources/swift_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true
require "test_helper"
require "tmpdir"
require "pp"

if Licensed::Shell.tool_available?("swift")
describe Licensed::Sources::Swift do
let(:fixtures) { File.expand_path("../../fixtures/swift", __FILE__) }
let(:config) { Licensed::AppConfiguration.new({ "source_path" => Dir.pwd }) }
let(:source) { Licensed::Sources::Swift.new(config) }

describe "enabled?" do
it "is true if Swift package exists" do
Dir.chdir(fixtures) do
assert source.enabled?
end
end

it "is false if Swift package doesn't exist" do
Dir.chdir(Dir.tmpdir) do
refute source.enabled?
end
end
end

describe "enumerate_dependencies" do
it "does not include the source project" do
Dir.chdir(fixtures) do
config["name"] = "Fixtures"
refute source.enumerate_dependencies.find { |d| d.name == "Fixtures" }
end
end

it "finds dependencies from path sources" do
Dir.chdir(fixtures) do
dep = source.enumerate_dependencies.find { |d| d.name == "DeckOfPlayingCards" }
assert dep
assert_equal "3.0.4", dep.version

dep = source.enumerate_dependencies.find { |d| d.name == "FisherYates" }
assert dep
assert_equal "2.0.6", dep.version

dep = source.enumerate_dependencies.find { |d| d.name == "PlayingCard" }
assert dep
assert_equal "3.0.5", dep.version

dep = source.enumerate_dependencies.find { |d| d.name == "Invalid" }
refute dep
end
end

it "handles invalid repositoryURL field" do
source.stubs(:pins).returns(
JSON.parse <<-JSON
[{
"package": "Invalid",
"repositoryURL": "Invalid",
"state": {
"version": "1.0.0"
}
}]
JSON
)

dep = source.enumerate_dependencies.find { |d| d.name == "Invalid" }
assert dep
assert dep.errors
end

it "handles invalid Package.resolved file" do
Dir.mktmpdir do |dir|
FileUtils.cp_r(fixtures, dir)
File.write(File.join(dir, "Package.resolved"), %("Invalid"))

Dir.chdir(dir) do
assert_raises ::Licensed::Sources::Source::Error do
source.enumerate_dependencies
end
end
end
end
end
end
end