Skip to content

Commit

Permalink
add backwards compatibility for workspace-state.json v4 (#3803)
Browse files Browse the repository at this point in the history
motivation: some tools rely on using older SwiftPM to generate the state, so SwiftPM must be able to read older formats

changes:
* add deserialization for v4 and number the new format as v5
* add tests for v4 and v5 deserialization

rdar://84172027
  • Loading branch information
tomerd committed Oct 13, 2021
1 parent 0a49bb9 commit f2dd6ce
Show file tree
Hide file tree
Showing 2 changed files with 481 additions and 13 deletions.
268 changes: 257 additions & 11 deletions Sources/Workspace/WorkspaceState.swift
Expand Up @@ -97,6 +97,11 @@ fileprivate struct WorkspaceStateStorage {
let dependencies = try v4.object.dependencies.map{ try Workspace.ManagedDependency($0) }
let artifacts = try v4.object.artifacts.map{ try Workspace.ManagedArtifact($0) }
return (dependencies: .init(dependencies), artifacts: .init(artifacts))
case 5:
let v5 = try self.decoder.decode(path: self.path, fileSystem: self.fileSystem, as: V5.self)
let dependencies = try v5.object.dependencies.map{ try Workspace.ManagedDependency($0) }
let artifacts = try v5.object.artifacts.map{ try Workspace.ManagedArtifact($0) }
return (dependencies: .init(dependencies), artifacts: .init(artifacts))
default:
throw StringError("unknown 'WorkspaceStateStorage' version '\(version.version)' at '\(self.path)'")
}
Expand All @@ -109,7 +114,7 @@ fileprivate struct WorkspaceStateStorage {
}

try self.fileSystem.withLock(on: self.path, type: .exclusive) {
let storage = V4(dependencies: dependencies, artifacts: artifacts)
let storage = V5(dependencies: dependencies, artifacts: artifacts)

let data = try self.encoder.encode(storage)
try self.fileSystem.writeFileContents(self.path, data: data)
Expand All @@ -128,23 +133,25 @@ fileprivate struct WorkspaceStateStorage {
func fileExists() -> Bool {
return self.fileSystem.exists(self.path)
}
}

extension WorkspaceStateStorage {
// version reader
struct Version: Codable {
let version: Int
}
}

/// * 4: Artifacts.
/// * 3: Package kind.
/// * 2: Package identity.
/// * 1: Initial version.
// v4 storage format
struct V4: Codable {
// MARK: - V5 format

extension WorkspaceStateStorage {
// v5 storage format
struct V5: Codable {
let version: Int
let object: Container

init (dependencies: Workspace.ManagedDependencies, artifacts: Workspace.ManagedArtifacts) {
self.version = 4
self.version = 5
self.object = .init(
dependencies: dependencies.map { .init($0) }.sorted { $0.packageRef.identity < $1.packageRef.identity },
artifacts: artifacts.map { .init($0) }.sorted { $0.packageRef.identity < $1.packageRef.identity }
Expand Down Expand Up @@ -377,7 +384,7 @@ fileprivate struct WorkspaceStateStorage {
}

extension Workspace.ManagedDependency {
fileprivate init(_ dependency: WorkspaceStateStorage.V4.Dependency) throws {
fileprivate init(_ dependency: WorkspaceStateStorage.V5.Dependency) throws {
try self.init(
packageRef: .init(dependency.packageRef),
state: dependency.state.underlying,
Expand All @@ -387,7 +394,7 @@ extension Workspace.ManagedDependency {
}

extension Workspace.ManagedArtifact {
fileprivate init(_ artifact: WorkspaceStateStorage.V4.Artifact) throws {
fileprivate init(_ artifact: WorkspaceStateStorage.V5.Artifact) throws {
try self.init(
packageRef: .init(artifact.packageRef),
targetName: artifact.targetName,
Expand All @@ -398,7 +405,7 @@ extension Workspace.ManagedArtifact {
}

extension PackageModel.PackageReference {
fileprivate init(_ reference: WorkspaceStateStorage.V4.PackageReference) throws {
fileprivate init(_ reference: WorkspaceStateStorage.V5.PackageReference) throws {
let identity = PackageIdentity.plain(reference.identity)
let kind: PackageModel.PackageReference.Kind
switch reference.kind {
Expand All @@ -425,6 +432,245 @@ extension PackageModel.PackageReference {
}
}

extension CheckoutState {
fileprivate init(_ state: WorkspaceStateStorage.V5.Dependency.State.CheckoutInfo) throws {
let revision: Revision = .init(identifier: state.revision)
if let branch = state.branch {
self = .branch(name: branch, revision: revision)
} else if let version = state.version {
self = try .version(Version(versionString: version), revision: revision)
} else {
self = .revision(revision)
}
}
}


// MARK: - V1...4 format

extension WorkspaceStateStorage {
/// * 4: Artifacts.
/// * 3: Package kind.
/// * 2: Package identity.
/// * 1: Initial version.
// v4 storage format
struct V4: Decodable {
let version: Int
let object: Container

struct Container: Decodable {
var dependencies: [Dependency]
var artifacts: [Artifact]
}

struct Dependency: Decodable {
let packageRef: PackageReference
let state: State
let subpath: String

init(packageRef: PackageReference, state: State, subpath: String) {
self.packageRef = packageRef
self.state = state
self.subpath = subpath
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let packageRef = try container.decode(PackageReference.self, forKey: .packageRef)
let subpath = try container.decode(String.self, forKey: .subpath)
let basedOn = try container.decode(Dependency?.self, forKey: .basedOn)
let state = try State.decode(
container: container.nestedContainer(keyedBy: State.CodingKeys.self, forKey: .state),
packageRef: packageRef,
basedOn: basedOn
)

self.init(
packageRef: packageRef,
state: state,
subpath: subpath
)
}

enum CodingKeys: CodingKey {
case packageRef
case state
case subpath
case basedOn
}

struct State {
let underlying: Workspace.ManagedDependency.State

init(underlying: Workspace.ManagedDependency.State) {
self.underlying = underlying
}

static func decode(container: KeyedDecodingContainer<Self.CodingKeys>, packageRef: PackageReference, basedOn: Dependency?) throws -> State {
let kind = try container.decode(String.self, forKey: .name)
switch kind {
case "local":
return try self.init(underlying: .local(.init(validating: packageRef.location)))
case "checkout":
let checkout = try container.decode(CheckoutInfo.self, forKey: .checkoutState)
return try self.init(underlying: .checkout(.init(checkout)))
case "edited":
let path = try container.decode(AbsolutePath?.self, forKey: .path)
return try self.init(underlying: .edited(basedOn: basedOn.map { try .init($0) }, unmanagedPath: path))
default:
throw InternalError("unknown checkout state \(kind)")
}
}

enum CodingKeys: CodingKey {
case name
case path
case checkoutState
}

struct CheckoutInfo: Codable {
let revision: String
let branch: String?
let version: String?

init(_ state: CheckoutState) {
switch state {
case .version(let version, let revision):
self.version = version.description
self.branch = nil
self.revision = revision.identifier
case .branch(let branch, let revision):
self.version = nil
self.branch = branch
self.revision = revision.identifier
case .revision(let revision):
self.version = nil
self.branch = nil
self.revision = revision.identifier
}
}
}
}
}

struct Artifact: Decodable {
let packageRef: PackageReference
let targetName: String
let source: Source
let path: String

struct Source: Decodable {
let underlying: Workspace.ManagedArtifact.Source

init(underlying: Workspace.ManagedArtifact.Source) {
self.underlying = underlying
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let kind = try container.decode(String.self, forKey: .type)
switch kind {
case "local":
let checksum = try container.decodeIfPresent(String.self, forKey: .checksum)
self.init(underlying: .local(checksum: checksum))
case "remote":
let url = try container.decode(String.self, forKey: .url)
let checksum = try container.decode(String.self, forKey: .checksum)
self.init(underlying: .remote(url: url, checksum: checksum))
default:
throw InternalError("unknown checkout state \(kind)")
}
}

enum CodingKeys: CodingKey {
case type
case url
case checksum
}
}
}

struct PackageReference: Decodable {
let identity: String
let kind: String
let location: String
let name: String

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.identity = try container.decode(String.self, forKey: .identity)
self.kind = try container.decode(String.self, forKey: .kind)
self.name = try container.decode(String.self, forKey: .name)
if let location = try container.decodeIfPresent(String.self, forKey: .location) {
self.location = location
} else if let path = try container.decodeIfPresent(String.self, forKey: .path) {
self.location = path
} else {
throw StringError("invalid package ref, missing location and path")
}
}

enum CodingKeys: CodingKey {
case identity
case kind
case location
case path
case name
}
}
}
}

extension Workspace.ManagedDependency {
fileprivate init(_ dependency: WorkspaceStateStorage.V4.Dependency) throws {
try self.init(
packageRef: .init(dependency.packageRef),
state: dependency.state.underlying,
subpath: RelativePath(dependency.subpath)
)
}
}

extension Workspace.ManagedArtifact {
fileprivate init(_ artifact: WorkspaceStateStorage.V4.Artifact) throws {
try self.init(
packageRef: .init(artifact.packageRef),
targetName: artifact.targetName,
source: artifact.source.underlying,
path: AbsolutePath(artifact.path)
)
}
}

extension PackageModel.PackageReference {
fileprivate init(_ reference: WorkspaceStateStorage.V4.PackageReference) throws {
let identity = PackageIdentity.plain(reference.identity)
let kind: PackageModel.PackageReference.Kind
switch reference.kind {
case "root":
kind = try .root(.init(validating: reference.location))
case "local":
kind = try .fileSystem(.init(validating: reference.location))
case "remote":
if let path = try? AbsolutePath(validating: reference.location) {
kind = .localSourceControl(path)
} else if let url = URL(string: reference.location) {
kind = .remoteSourceControl(url)
} else {
throw StringError("invalid package kind \(reference.kind)")
}
default:
throw StringError("invalid package kind \(reference.kind)")
}

self.init(
identity: identity,
kind: kind,
name: reference.name
)
}
}

extension CheckoutState {
fileprivate init(_ state: WorkspaceStateStorage.V4.Dependency.State.CheckoutInfo) throws {
let revision: Revision = .init(identifier: state.revision)
Expand Down

0 comments on commit f2dd6ce

Please sign in to comment.