spm_version_updates 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/lib/spm_version_updates/allow_host_normalizer.rb +53 -0
- data/lib/spm_version_updates/credential_redactor.rb +16 -0
- data/lib/spm_version_updates/fail_on_threshold.rb +45 -0
- data/lib/spm_version_updates/git_host_normalizer.rb +63 -0
- data/lib/spm_version_updates/git_operations.rb +150 -0
- data/lib/spm_version_updates/manifest_parser.rb +258 -0
- data/lib/spm_version_updates/package_resolved.rb +46 -0
- data/lib/spm_version_updates/repository_link.rb +112 -0
- data/lib/spm_version_updates/repository_update_rules.rb +222 -0
- data/lib/spm_version_updates/semver.rb +55 -0
- data/lib/spm_version_updates/spm_checker.rb +532 -0
- data/lib/spm_version_updates/spm_package_context.rb +49 -0
- data/lib/spm_version_updates/update_severity.rb +67 -0
- data/lib/spm_version_updates/upgrade_suggestion.rb +62 -0
- data/lib/spm_version_updates/version.rb +5 -0
- data/lib/spm_version_updates/version_tag_fetcher.rb +110 -0
- data/lib/spm_version_updates/version_tags_persistent_cache.rb +99 -0
- data/lib/spm_version_updates/xcode_parser.rb +71 -0
- data/lib/spm_version_updates/xcode_project_package_reader.rb +132 -0
- data/lib/spm_version_updates.rb +21 -0
- data/spm_version_updates.gemspec +46 -0
- metadata +79 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Derives per-update upgrade guidance from a package's requirement kind and the
|
|
4
|
+
# available version: the SwiftPM identity, a ready-to-run `swift package update`
|
|
5
|
+
# command (manifest mode only), and the manifest requirement change needed when
|
|
6
|
+
# the new version is outside the declared constraint. Shared between the GitHub
|
|
7
|
+
# Action reporters and the Danger plugin.
|
|
8
|
+
module UpgradeSuggestion
|
|
9
|
+
# SwiftPM's default package identity: the last path component of the
|
|
10
|
+
# repository URL, lowercased (the normalized URL already has no `.git`).
|
|
11
|
+
def self.identity(normalized_url)
|
|
12
|
+
normalized_url.to_s.split("/").last.to_s.downcase
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param package [SpmPackageContext]
|
|
16
|
+
# @param available_version [#to_s] the version (or commit) being suggested
|
|
17
|
+
# @param type [Symbol] :version, :above_maximum, :branch, or :revision
|
|
18
|
+
# @return [Hash] package_identity / requirement_kind / suggested_command /
|
|
19
|
+
# suggested_requirement, with inapplicable entries nil
|
|
20
|
+
def self.fields(package, available_version, type)
|
|
21
|
+
{
|
|
22
|
+
package_identity: identity(package.normalized_url),
|
|
23
|
+
requirement_kind: package.kind,
|
|
24
|
+
suggested_command: command(package),
|
|
25
|
+
suggested_requirement: requirement_change(package, available_version.to_s, type)
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `swift package update` only applies to Package.swift-managed dependencies
|
|
30
|
+
# (never Xcode projects, where source is nil) and cannot move a revision pin.
|
|
31
|
+
def self.command(package)
|
|
32
|
+
return nil unless package.source
|
|
33
|
+
return nil if package.kind == "revision"
|
|
34
|
+
|
|
35
|
+
"swift package update #{identity(package.normalized_url)}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The Package.swift requirement text needed before `swift package update` can
|
|
39
|
+
# reach the suggested version. In-range updates, branch pins, and revision
|
|
40
|
+
# pins need no manifest change.
|
|
41
|
+
def self.requirement_change(package, available, type)
|
|
42
|
+
return above_maximum_change(package, available) if type == :above_maximum
|
|
43
|
+
|
|
44
|
+
%(exact: "#{available}") if package.kind == "exactVersion"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.above_maximum_change(package, available)
|
|
48
|
+
case package.kind
|
|
49
|
+
when "exactVersion" then %(exact: "#{available}")
|
|
50
|
+
when "upToNextMajorVersion" then %(from: "#{available}")
|
|
51
|
+
when "upToNextMinorVersion" then %(.upToNextMinor(from: "#{available}"))
|
|
52
|
+
when "versionRange" then range_change(package.requirement, available)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
private_class_method :above_maximum_change
|
|
56
|
+
|
|
57
|
+
def self.range_change(requirement, available)
|
|
58
|
+
major = available[/\A(\d+)/, 1]
|
|
59
|
+
%("#{requirement['minimumVersion']}"..<"#{major.to_i + 1}.0.0") if major
|
|
60
|
+
end
|
|
61
|
+
private_class_method :range_change
|
|
62
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_operations"
|
|
4
|
+
|
|
5
|
+
# Fetches git tag versions concurrently for cache-key/repository URL lookup pairs.
|
|
6
|
+
class VersionTagFetcher
|
|
7
|
+
# Thread-safe result/error accumulator shared by fetcher workers.
|
|
8
|
+
FetchState = Struct.new(:mutex, :results, :errors, keyword_init: true) {
|
|
9
|
+
def self.build
|
|
10
|
+
new(mutex: Mutex.new, results: {}, errors: {})
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def record_result(cache_key, versions)
|
|
14
|
+
mutex.synchronize { results[cache_key] = versions }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def record_error(cache_key, error)
|
|
18
|
+
mutex.synchronize { errors[cache_key] = error }
|
|
19
|
+
end
|
|
20
|
+
}
|
|
21
|
+
private_constant :FetchState
|
|
22
|
+
|
|
23
|
+
# Re-raises worker lookup failures as one combined git lookup error.
|
|
24
|
+
LookupErrors = Struct.new(:errors) {
|
|
25
|
+
def raise_error
|
|
26
|
+
first_error = errors.first
|
|
27
|
+
|
|
28
|
+
raise(combined_error(first_error), cause: first_error) if first_error.kind_of?(GitOperations::LsRemoteError)
|
|
29
|
+
|
|
30
|
+
raise(first_error)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def combined_error(first_error)
|
|
36
|
+
GitOperations::LsRemoteError.new(message).tap { |error| error.set_backtrace(first_error.backtrace) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def message
|
|
40
|
+
errors.map(&:message).uniq.join("\n")
|
|
41
|
+
end
|
|
42
|
+
}
|
|
43
|
+
private_constant :LookupErrors
|
|
44
|
+
|
|
45
|
+
# Fetch all lookups, returning `[results, errors]`, both keyed by cache key.
|
|
46
|
+
#
|
|
47
|
+
# With `raise_on_error: true` (the default) any lookup failure is re-raised as
|
|
48
|
+
# one combined error after all workers finish; `errors` is then always empty.
|
|
49
|
+
# With `raise_on_error: false` failed lookups are returned in `errors` so the
|
|
50
|
+
# caller can degrade gracefully per package.
|
|
51
|
+
def self.call(lookups, worker_limit:, persistent_cache: nil, raise_on_error: true)
|
|
52
|
+
new(lookups, worker_limit, persistent_cache:, raise_on_error:).call
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(lookups, worker_limit, persistent_cache: nil, raise_on_error: true)
|
|
56
|
+
@lookups = lookups
|
|
57
|
+
@worker_limit = worker_limit
|
|
58
|
+
@persistent_cache = persistent_cache
|
|
59
|
+
@raise_on_error = raise_on_error
|
|
60
|
+
@state = FetchState.build
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call
|
|
64
|
+
queue = build_queue
|
|
65
|
+
workers(queue).each(&:value)
|
|
66
|
+
errors = @state.errors
|
|
67
|
+
raise_lookup_error if @raise_on_error && !errors.empty?
|
|
68
|
+
|
|
69
|
+
[@state.results, errors]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def build_queue
|
|
75
|
+
Queue.new.tap { |queue| @lookups.each { |lookup| queue << lookup } }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def workers(queue)
|
|
79
|
+
Array.new(worker_count) { Thread.new { drain_queue(queue) } }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def worker_count
|
|
83
|
+
[@worker_limit, @lookups.size].min
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def drain_queue(queue)
|
|
87
|
+
loop { fetch_lookup(queue) }
|
|
88
|
+
rescue ThreadError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def fetch_lookup(queue)
|
|
93
|
+
cache_key, repository_url, persistent_cache_key = queue.pop(true)
|
|
94
|
+
@state.record_result(cache_key, versions_for(repository_url, persistent_cache_key))
|
|
95
|
+
rescue GitOperations::LsRemoteError => error
|
|
96
|
+
@state.record_error(cache_key, error)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def versions_for(repository_url, persistent_cache_key)
|
|
100
|
+
cached_versions = @persistent_cache&.read(persistent_cache_key)
|
|
101
|
+
return cached_versions if cached_versions
|
|
102
|
+
|
|
103
|
+
GitOperations.version_tags(repository_url)
|
|
104
|
+
.tap { |versions| @persistent_cache&.write(persistent_cache_key, versions) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def raise_lookup_error
|
|
108
|
+
LookupErrors.new(@state.errors.values).raise_error
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
require_relative "credential_redactor"
|
|
8
|
+
require_relative "semver"
|
|
9
|
+
|
|
10
|
+
# Persistent on-disk cache for successful git tag lookups restored by actions/cache.
|
|
11
|
+
class VersionTagsPersistentCache
|
|
12
|
+
DEFAULT_TTL_SECONDS = 21_600
|
|
13
|
+
SCHEMA_VERSION = 1
|
|
14
|
+
|
|
15
|
+
def self.cache_key(normalized_url, repository_url)
|
|
16
|
+
Digest::SHA256.hexdigest("#{normalized_url}\n#{CredentialRedactor.redact(repository_url)}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(directory:, ttl_seconds:, clock: -> { Time.now.utc })
|
|
20
|
+
@directory = directory.to_s
|
|
21
|
+
@ttl_seconds = ttl_seconds.to_i
|
|
22
|
+
@clock = clock
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enabled?
|
|
26
|
+
!@directory.empty? && @ttl_seconds.positive?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read(cache_key)
|
|
30
|
+
return nil unless enabled?
|
|
31
|
+
|
|
32
|
+
record = read_record(cache_key)
|
|
33
|
+
return nil unless fresh_record?(record)
|
|
34
|
+
|
|
35
|
+
versions_from(record)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def write(cache_key, versions)
|
|
39
|
+
return unless enabled?
|
|
40
|
+
|
|
41
|
+
write_record(cache_key, versions)
|
|
42
|
+
rescue StandardError => error
|
|
43
|
+
warn("Failed to write to persistent cache: #{error.message}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def write_record(cache_key, versions)
|
|
49
|
+
temp = temp_path(cache_key)
|
|
50
|
+
FileUtils.mkdir_p(@directory)
|
|
51
|
+
File.write(temp, JSON.pretty_generate(record_for(versions)))
|
|
52
|
+
File.rename(temp, path_for(cache_key))
|
|
53
|
+
ensure
|
|
54
|
+
FileUtils.rm_f(temp) if temp
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_record(cache_key)
|
|
58
|
+
JSON.parse(File.read(path_for(cache_key)))
|
|
59
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fresh_record?(record)
|
|
64
|
+
return false unless record && record["schema_version"] == SCHEMA_VERSION
|
|
65
|
+
|
|
66
|
+
fetched_at = Time.iso8601(record.fetch("fetched_at"))
|
|
67
|
+
(@clock.call - fetched_at) <= @ttl_seconds
|
|
68
|
+
rescue StandardError
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def versions_from(record)
|
|
73
|
+
Array(record["tags"]).filter_map { |tag|
|
|
74
|
+
begin
|
|
75
|
+
SpmVersionUpdates::Semver.new(tag)
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
}
|
|
80
|
+
.sort
|
|
81
|
+
.reverse
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def record_for(versions)
|
|
85
|
+
{
|
|
86
|
+
"schema_version" => SCHEMA_VERSION,
|
|
87
|
+
"fetched_at" => @clock.call.iso8601,
|
|
88
|
+
"tags" => versions.map(&:to_s)
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def path_for(cache_key)
|
|
93
|
+
File.join(@directory, "#{cache_key}.json")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def temp_path(cache_key)
|
|
97
|
+
"#{path_for(cache_key)}.#{Process.pid}-#{Thread.current.object_id.abs}.tmp"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_operations"
|
|
4
|
+
require_relative "package_resolved"
|
|
5
|
+
require_relative "xcode_project_package_reader"
|
|
6
|
+
|
|
7
|
+
# Xcode project and Package.resolved parsing (migrated from xcode.rb)
|
|
8
|
+
module XcodeParser
|
|
9
|
+
# Find the configured SPM dependencies in the xcodeproj.
|
|
10
|
+
#
|
|
11
|
+
# Keyed by the normalized repository URL (used to match against
|
|
12
|
+
# `Package.resolved` pins and `ignore-repos`), while the original,
|
|
13
|
+
# scheme-bearing `repository_url` is retained for git operations.
|
|
14
|
+
#
|
|
15
|
+
# @param [String] xcodeproj_path The path of the Xcode project
|
|
16
|
+
# @return [Hash<String, Hash>] normalized URL => { "repository_url", "requirement" }
|
|
17
|
+
def self.get_packages(xcodeproj_path)
|
|
18
|
+
raise(XcodeprojPathMustBeSet) if xcodeproj_path.nil? || xcodeproj_path.empty?
|
|
19
|
+
|
|
20
|
+
XcodeProjectPackageReader.package_references(xcodeproj_path).to_h { |package|
|
|
21
|
+
repository_url = package.repository_url
|
|
22
|
+
[
|
|
23
|
+
GitOperations.trim_repo_url(repository_url),
|
|
24
|
+
{ "repository_url" => repository_url, "requirement" => package.requirement },
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Extracts resolved versions from Package.resolved relative to an Xcode project.
|
|
30
|
+
# When a block is given, malformed resolved files are reported to it as
|
|
31
|
+
# `(resolved_path, error)` and skipped; without a block the error is raised.
|
|
32
|
+
# @param [String] xcodeproj_path The path to your Xcode project
|
|
33
|
+
# @raise [CouldNotFindResolvedFile] if no Package.resolved files were found
|
|
34
|
+
# @raise [PackageResolved::MalformedFileError] if a resolved file is invalid JSON and no block is given
|
|
35
|
+
# @return [Hash<String, String>]
|
|
36
|
+
def self.get_resolved_versions(xcodeproj_path)
|
|
37
|
+
resolved_paths = find_packages_resolved_file(xcodeproj_path)
|
|
38
|
+
raise(CouldNotFindResolvedFile) if resolved_paths.empty?
|
|
39
|
+
|
|
40
|
+
resolved_paths.each_with_object({}) { |resolved_path, pins|
|
|
41
|
+
begin
|
|
42
|
+
pins.merge!(PackageResolved.versions_from(resolved_path))
|
|
43
|
+
rescue PackageResolved::MalformedFileError => error
|
|
44
|
+
raise unless block_given?
|
|
45
|
+
|
|
46
|
+
yield(resolved_path, error)
|
|
47
|
+
end
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Find the Packages.resolved file
|
|
52
|
+
# @return [Array<String>]
|
|
53
|
+
def self.find_packages_resolved_file(xcodeproj_path)
|
|
54
|
+
checked = XcodeProjectPackageReader.package_resolved_candidate_paths(xcodeproj_path)
|
|
55
|
+
locations = checked.select { |path| File.exist?(path) }
|
|
56
|
+
|
|
57
|
+
puts("Checked Package.resolved paths: #{checked}")
|
|
58
|
+
puts("Found Package.resolved paths: #{locations}")
|
|
59
|
+
locations
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private_class_method :find_packages_resolved_file
|
|
63
|
+
|
|
64
|
+
# Raised when Xcode project mode is invoked without a project path.
|
|
65
|
+
class XcodeprojPathMustBeSet < StandardError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raised when an Xcode project does not have a Package.resolved file.
|
|
69
|
+
class CouldNotFindResolvedFile < StandardError
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
autoload :Xcodeproj, "xcodeproj"
|
|
4
|
+
|
|
5
|
+
# Reads Swift package references and adjacent Package.resolved locations for an
|
|
6
|
+
# Xcode project without requiring Xcode to be installed.
|
|
7
|
+
module XcodeProjectPackageReader
|
|
8
|
+
# Lightweight package reference read from either project objects or pbxproj data.
|
|
9
|
+
PackageReference = Struct.new(:repository_url, :requirement, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
# Builds the lightweight pbxproj parser fallback error list without triggering autoloads.
|
|
12
|
+
module PbxprojFallbackErrors
|
|
13
|
+
def self.to_a
|
|
14
|
+
[
|
|
15
|
+
SystemCallError,
|
|
16
|
+
IOError,
|
|
17
|
+
loaded_nested_constant(:Xcodeproj, :Informative),
|
|
18
|
+
loaded_nested_constant(:Nanaimo, :Error),
|
|
19
|
+
loaded_nested_constant(:CFPropertyList, :CFPlistError),
|
|
20
|
+
].compact
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.loaded_nested_constant(parent_name, child_name)
|
|
24
|
+
parent = loaded_constant(Object, parent_name)
|
|
25
|
+
loaded_constant(parent, child_name) if parent
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.loaded_constant(namespace, name)
|
|
29
|
+
return unless namespace.kind_of?(Module)
|
|
30
|
+
return if namespace.autoload?(name)
|
|
31
|
+
return unless namespace.const_defined?(name, false)
|
|
32
|
+
|
|
33
|
+
namespace.const_get(name, false)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_constant :PackageReference, :PbxprojFallbackErrors
|
|
38
|
+
|
|
39
|
+
def self.package_references(xcodeproj_path)
|
|
40
|
+
package_references_from_pbxproj(xcodeproj_path) || package_references_from_project(xcodeproj_path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.package_resolved_candidate_paths(xcodeproj_path)
|
|
44
|
+
[
|
|
45
|
+
workspace_resolved_path(xcodeproj_path),
|
|
46
|
+
File.join(xcodeproj_path, "project.xcworkspace", "xcshareddata", "swiftpm", "Package.resolved"),
|
|
47
|
+
].compact
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.package_references_from_pbxproj(xcodeproj_path)
|
|
51
|
+
read_existing_pbxproj(xcodeproj_path)
|
|
52
|
+
rescue *PbxprojFallbackErrors.to_a => error
|
|
53
|
+
warn(pbxproj_fallback_message(pbxproj_path_for(xcodeproj_path), error))
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.read_existing_pbxproj(xcodeproj_path)
|
|
58
|
+
pbxproj_path = existing_pbxproj_path(xcodeproj_path)
|
|
59
|
+
return unless pbxproj_path
|
|
60
|
+
|
|
61
|
+
package_references_from_pbxproj_objects(pbxproj_objects(pbxproj_path))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.existing_pbxproj_path(xcodeproj_path)
|
|
65
|
+
pbxproj_path = pbxproj_path_for(xcodeproj_path)
|
|
66
|
+
pbxproj_path if File.file?(pbxproj_path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.pbxproj_fallback_message(pbxproj_path, error)
|
|
70
|
+
"WARNING: Could not read #{pbxproj_path} with the lightweight pbxproj parser " \
|
|
71
|
+
"(#{error.class}: #{error.message}); falling back to full Xcode project parsing."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.pbxproj_path_for(xcodeproj_path)
|
|
75
|
+
File.join(xcodeproj_path, "project.pbxproj")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.pbxproj_objects(pbxproj_path)
|
|
79
|
+
Xcodeproj::Plist.read_from_path(pbxproj_path).fetch("objects", {}).values
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.package_references_from_pbxproj_objects(objects)
|
|
83
|
+
objects.filter_map { |object| package_reference_from_plist_object(object) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.package_reference_from_plist_object(object)
|
|
87
|
+
return unless object["isa"] == "XCRemoteSwiftPackageReference"
|
|
88
|
+
|
|
89
|
+
repository_url = object["repositoryURL"]
|
|
90
|
+
return if repository_url.to_s.strip.empty?
|
|
91
|
+
|
|
92
|
+
PackageReference.new(repository_url:, requirement: object["requirement"])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.package_references_from_project(xcodeproj_path)
|
|
96
|
+
package_references_from_project_objects(Xcodeproj::Project.open(xcodeproj_path).objects)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.package_references_from_project_objects(objects)
|
|
100
|
+
objects.filter_map { |object| package_reference_from_project_object(object) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.package_reference_from_project_object(object)
|
|
104
|
+
return unless object.kind_of?(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
|
|
105
|
+
|
|
106
|
+
repository_url = object.repositoryURL
|
|
107
|
+
return if repository_url.to_s.strip.empty?
|
|
108
|
+
|
|
109
|
+
PackageReference.new(repository_url:, requirement: object.requirement)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.workspace_resolved_path(xcodeproj_path)
|
|
113
|
+
path = xcodeproj_path.to_s.sub(%r{/+\z}, "")
|
|
114
|
+
return unless path.end_with?(".xcodeproj")
|
|
115
|
+
|
|
116
|
+
workspace = path.sub(/\.xcodeproj\z/, ".xcworkspace")
|
|
117
|
+
File.join(workspace, "xcshareddata", "swiftpm", "Package.resolved")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private_class_method :package_references_from_pbxproj,
|
|
121
|
+
:read_existing_pbxproj,
|
|
122
|
+
:existing_pbxproj_path,
|
|
123
|
+
:pbxproj_fallback_message,
|
|
124
|
+
:pbxproj_path_for,
|
|
125
|
+
:pbxproj_objects,
|
|
126
|
+
:package_references_from_pbxproj_objects,
|
|
127
|
+
:package_reference_from_plist_object,
|
|
128
|
+
:package_references_from_project,
|
|
129
|
+
:package_references_from_project_objects,
|
|
130
|
+
:package_reference_from_project_object,
|
|
131
|
+
:workspace_resolved_path
|
|
132
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spm_version_updates/allow_host_normalizer"
|
|
4
|
+
require_relative "spm_version_updates/credential_redactor"
|
|
5
|
+
require_relative "spm_version_updates/fail_on_threshold"
|
|
6
|
+
require_relative "spm_version_updates/git_host_normalizer"
|
|
7
|
+
require_relative "spm_version_updates/git_operations"
|
|
8
|
+
require_relative "spm_version_updates/manifest_parser"
|
|
9
|
+
require_relative "spm_version_updates/package_resolved"
|
|
10
|
+
require_relative "spm_version_updates/repository_link"
|
|
11
|
+
require_relative "spm_version_updates/repository_update_rules"
|
|
12
|
+
require_relative "spm_version_updates/semver"
|
|
13
|
+
require_relative "spm_version_updates/spm_checker"
|
|
14
|
+
require_relative "spm_version_updates/spm_package_context"
|
|
15
|
+
require_relative "spm_version_updates/update_severity"
|
|
16
|
+
require_relative "spm_version_updates/upgrade_suggestion"
|
|
17
|
+
require_relative "spm_version_updates/version"
|
|
18
|
+
require_relative "spm_version_updates/version_tag_fetcher"
|
|
19
|
+
require_relative "spm_version_updates/version_tags_persistent_cache"
|
|
20
|
+
require_relative "spm_version_updates/xcode_parser"
|
|
21
|
+
require_relative "spm_version_updates/xcode_project_package_reader"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require "spm_version_updates/version"
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = "spm_version_updates"
|
|
9
|
+
spec.version = SpmVersionUpdates::VERSION
|
|
10
|
+
spec.authors = ["Harold Martin"]
|
|
11
|
+
spec.email = ["harold.martin@gmail.com"]
|
|
12
|
+
spec.description = "Detect available updates to Swift Package Manager dependencies " \
|
|
13
|
+
"from Package.swift manifests or Xcode projects."
|
|
14
|
+
spec.summary = "Core library for checking Swift Package Manager dependency updates."
|
|
15
|
+
spec.homepage = "https://github.com/hbmartin/github-action-spm_version_updates"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.2"
|
|
18
|
+
|
|
19
|
+
release_paths = [
|
|
20
|
+
"LICENSE.txt",
|
|
21
|
+
"README.md",
|
|
22
|
+
"spm_version_updates.gemspec",
|
|
23
|
+
]
|
|
24
|
+
spec.files = begin
|
|
25
|
+
git_files = begin
|
|
26
|
+
IO.popen(
|
|
27
|
+
["git", "-C", __dir__, "ls-files", "-z", "lib", *release_paths],
|
|
28
|
+
err: File::NULL,
|
|
29
|
+
&:read
|
|
30
|
+
)
|
|
31
|
+
.split("\x0")
|
|
32
|
+
.reject(&:empty?)
|
|
33
|
+
rescue Errno::ENOENT
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
fallback_files = Dir.glob(["lib/**/*", *release_paths], base: __dir__)
|
|
37
|
+
.select { |path| File.file?(File.join(__dir__, path)) }
|
|
38
|
+
(git_files.empty? ? fallback_files : git_files).sort
|
|
39
|
+
end
|
|
40
|
+
spec.require_paths = ["lib"]
|
|
41
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
42
|
+
|
|
43
|
+
spec.add_runtime_dependency("semverify", "~> 0.3")
|
|
44
|
+
# Xcode-project mode additionally requires the "xcodeproj" gem (loaded
|
|
45
|
+
# lazily); manifest mode works without it.
|
|
46
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spm_version_updates
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Harold Martin
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: semverify
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.3'
|
|
26
|
+
description: Detect available updates to Swift Package Manager dependencies from Package.swift
|
|
27
|
+
manifests or Xcode projects.
|
|
28
|
+
email:
|
|
29
|
+
- harold.martin@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/spm_version_updates.rb
|
|
37
|
+
- lib/spm_version_updates/allow_host_normalizer.rb
|
|
38
|
+
- lib/spm_version_updates/credential_redactor.rb
|
|
39
|
+
- lib/spm_version_updates/fail_on_threshold.rb
|
|
40
|
+
- lib/spm_version_updates/git_host_normalizer.rb
|
|
41
|
+
- lib/spm_version_updates/git_operations.rb
|
|
42
|
+
- lib/spm_version_updates/manifest_parser.rb
|
|
43
|
+
- lib/spm_version_updates/package_resolved.rb
|
|
44
|
+
- lib/spm_version_updates/repository_link.rb
|
|
45
|
+
- lib/spm_version_updates/repository_update_rules.rb
|
|
46
|
+
- lib/spm_version_updates/semver.rb
|
|
47
|
+
- lib/spm_version_updates/spm_checker.rb
|
|
48
|
+
- lib/spm_version_updates/spm_package_context.rb
|
|
49
|
+
- lib/spm_version_updates/update_severity.rb
|
|
50
|
+
- lib/spm_version_updates/upgrade_suggestion.rb
|
|
51
|
+
- lib/spm_version_updates/version.rb
|
|
52
|
+
- lib/spm_version_updates/version_tag_fetcher.rb
|
|
53
|
+
- lib/spm_version_updates/version_tags_persistent_cache.rb
|
|
54
|
+
- lib/spm_version_updates/xcode_parser.rb
|
|
55
|
+
- lib/spm_version_updates/xcode_project_package_reader.rb
|
|
56
|
+
- spm_version_updates.gemspec
|
|
57
|
+
homepage: https://github.com/hbmartin/github-action-spm_version_updates
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.2'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 4.0.10
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Core library for checking Swift Package Manager dependency updates.
|
|
79
|
+
test_files: []
|