Skip to content

Commit f2dd6ce

Browse files
authored
add backwards compatibility for workspace-state.json v4 (#3803)
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
1 parent 0a49bb9 commit f2dd6ce

File tree

2 files changed

+481
-13
lines changed

2 files changed

+481
-13
lines changed

Sources/Workspace/WorkspaceState.swift

Lines changed: 257 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ fileprivate struct WorkspaceStateStorage {
9797
let dependencies = try v4.object.dependencies.map{ try Workspace.ManagedDependency($0) }
9898
let artifacts = try v4.object.artifacts.map{ try Workspace.ManagedArtifact($0) }
9999
return (dependencies: .init(dependencies), artifacts: .init(artifacts))
100+
case 5:
101+
let v5 = try self.decoder.decode(path: self.path, fileSystem: self.fileSystem, as: V5.self)
102+
let dependencies = try v5.object.dependencies.map{ try Workspace.ManagedDependency($0) }
103+
let artifacts = try v5.object.artifacts.map{ try Workspace.ManagedArtifact($0) }
104+
return (dependencies: .init(dependencies), artifacts: .init(artifacts))
100105
default:
101106
throw StringError("unknown 'WorkspaceStateStorage' version '\(version.version)' at '\(self.path)'")
102107
}
@@ -109,7 +114,7 @@ fileprivate struct WorkspaceStateStorage {
109114
}
110115

111116
try self.fileSystem.withLock(on: self.path, type: .exclusive) {
112-
let storage = V4(dependencies: dependencies, artifacts: artifacts)
117+
let storage = V5(dependencies: dependencies, artifacts: artifacts)
113118

114119
let data = try self.encoder.encode(storage)
115120
try self.fileSystem.writeFileContents(self.path, data: data)
@@ -128,23 +133,25 @@ fileprivate struct WorkspaceStateStorage {
128133
func fileExists() -> Bool {
129134
return self.fileSystem.exists(self.path)
130135
}
136+
}
131137

138+
extension WorkspaceStateStorage {
132139
// version reader
133140
struct Version: Codable {
134141
let version: Int
135142
}
143+
}
136144

137-
/// * 4: Artifacts.
138-
/// * 3: Package kind.
139-
/// * 2: Package identity.
140-
/// * 1: Initial version.
141-
// v4 storage format
142-
struct V4: Codable {
145+
// MARK: - V5 format
146+
147+
extension WorkspaceStateStorage {
148+
// v5 storage format
149+
struct V5: Codable {
143150
let version: Int
144151
let object: Container
145152

146153
init (dependencies: Workspace.ManagedDependencies, artifacts: Workspace.ManagedArtifacts) {
147-
self.version = 4
154+
self.version = 5
148155
self.object = .init(
149156
dependencies: dependencies.map { .init($0) }.sorted { $0.packageRef.identity < $1.packageRef.identity },
150157
artifacts: artifacts.map { .init($0) }.sorted { $0.packageRef.identity < $1.packageRef.identity }
@@ -377,7 +384,7 @@ fileprivate struct WorkspaceStateStorage {
377384
}
378385

379386
extension Workspace.ManagedDependency {
380-
fileprivate init(_ dependency: WorkspaceStateStorage.V4.Dependency) throws {
387+
fileprivate init(_ dependency: WorkspaceStateStorage.V5.Dependency) throws {
381388
try self.init(
382389
packageRef: .init(dependency.packageRef),
383390
state: dependency.state.underlying,
@@ -387,7 +394,7 @@ extension Workspace.ManagedDependency {
387394
}
388395

389396
extension Workspace.ManagedArtifact {
390-
fileprivate init(_ artifact: WorkspaceStateStorage.V4.Artifact) throws {
397+
fileprivate init(_ artifact: WorkspaceStateStorage.V5.Artifact) throws {
391398
try self.init(
392399
packageRef: .init(artifact.packageRef),
393400
targetName: artifact.targetName,
@@ -398,7 +405,7 @@ extension Workspace.ManagedArtifact {
398405
}
399406

400407
extension PackageModel.PackageReference {
401-
fileprivate init(_ reference: WorkspaceStateStorage.V4.PackageReference) throws {
408+
fileprivate init(_ reference: WorkspaceStateStorage.V5.PackageReference) throws {
402409
let identity = PackageIdentity.plain(reference.identity)
403410
let kind: PackageModel.PackageReference.Kind
404411
switch reference.kind {
@@ -425,6 +432,245 @@ extension PackageModel.PackageReference {
425432
}
426433
}
427434

435+
extension CheckoutState {
436+
fileprivate init(_ state: WorkspaceStateStorage.V5.Dependency.State.CheckoutInfo) throws {
437+
let revision: Revision = .init(identifier: state.revision)
438+
if let branch = state.branch {
439+
self = .branch(name: branch, revision: revision)
440+
} else if let version = state.version {
441+
self = try .version(Version(versionString: version), revision: revision)
442+
} else {
443+
self = .revision(revision)
444+
}
445+
}
446+
}
447+
448+
449+
// MARK: - V1...4 format
450+
451+
extension WorkspaceStateStorage {
452+
/// * 4: Artifacts.
453+
/// * 3: Package kind.
454+
/// * 2: Package identity.
455+
/// * 1: Initial version.
456+
// v4 storage format
457+
struct V4: Decodable {
458+
let version: Int
459+
let object: Container
460+
461+
struct Container: Decodable {
462+
var dependencies: [Dependency]
463+
var artifacts: [Artifact]
464+
}
465+
466+
struct Dependency: Decodable {
467+
let packageRef: PackageReference
468+
let state: State
469+
let subpath: String
470+
471+
init(packageRef: PackageReference, state: State, subpath: String) {
472+
self.packageRef = packageRef
473+
self.state = state
474+
self.subpath = subpath
475+
}
476+
477+
init(from decoder: Decoder) throws {
478+
let container = try decoder.container(keyedBy: CodingKeys.self)
479+
let packageRef = try container.decode(PackageReference.self, forKey: .packageRef)
480+
let subpath = try container.decode(String.self, forKey: .subpath)
481+
let basedOn = try container.decode(Dependency?.self, forKey: .basedOn)
482+
let state = try State.decode(
483+
container: container.nestedContainer(keyedBy: State.CodingKeys.self, forKey: .state),
484+
packageRef: packageRef,
485+
basedOn: basedOn
486+
)
487+
488+
self.init(
489+
packageRef: packageRef,
490+
state: state,
491+
subpath: subpath
492+
)
493+
}
494+
495+
enum CodingKeys: CodingKey {
496+
case packageRef
497+
case state
498+
case subpath
499+
case basedOn
500+
}
501+
502+
struct State {
503+
let underlying: Workspace.ManagedDependency.State
504+
505+
init(underlying: Workspace.ManagedDependency.State) {
506+
self.underlying = underlying
507+
}
508+
509+
static func decode(container: KeyedDecodingContainer<Self.CodingKeys>, packageRef: PackageReference, basedOn: Dependency?) throws -> State {
510+
let kind = try container.decode(String.self, forKey: .name)
511+
switch kind {
512+
case "local":
513+
return try self.init(underlying: .local(.init(validating: packageRef.location)))
514+
case "checkout":
515+
let checkout = try container.decode(CheckoutInfo.self, forKey: .checkoutState)
516+
return try self.init(underlying: .checkout(.init(checkout)))
517+
case "edited":
518+
let path = try container.decode(AbsolutePath?.self, forKey: .path)
519+
return try self.init(underlying: .edited(basedOn: basedOn.map { try .init($0) }, unmanagedPath: path))
520+
default:
521+
throw InternalError("unknown checkout state \(kind)")
522+
}
523+
}
524+
525+
enum CodingKeys: CodingKey {
526+
case name
527+
case path
528+
case checkoutState
529+
}
530+
531+
struct CheckoutInfo: Codable {
532+
let revision: String
533+
let branch: String?
534+
let version: String?
535+
536+
init(_ state: CheckoutState) {
537+
switch state {
538+
case .version(let version, let revision):
539+
self.version = version.description
540+
self.branch = nil
541+
self.revision = revision.identifier
542+
case .branch(let branch, let revision):
543+
self.version = nil
544+
self.branch = branch
545+
self.revision = revision.identifier
546+
case .revision(let revision):
547+
self.version = nil
548+
self.branch = nil
549+
self.revision = revision.identifier
550+
}
551+
}
552+
}
553+
}
554+
}
555+
556+
struct Artifact: Decodable {
557+
let packageRef: PackageReference
558+
let targetName: String
559+
let source: Source
560+
let path: String
561+
562+
struct Source: Decodable {
563+
let underlying: Workspace.ManagedArtifact.Source
564+
565+
init(underlying: Workspace.ManagedArtifact.Source) {
566+
self.underlying = underlying
567+
}
568+
569+
init(from decoder: Decoder) throws {
570+
let container = try decoder.container(keyedBy: CodingKeys.self)
571+
let kind = try container.decode(String.self, forKey: .type)
572+
switch kind {
573+
case "local":
574+
let checksum = try container.decodeIfPresent(String.self, forKey: .checksum)
575+
self.init(underlying: .local(checksum: checksum))
576+
case "remote":
577+
let url = try container.decode(String.self, forKey: .url)
578+
let checksum = try container.decode(String.self, forKey: .checksum)
579+
self.init(underlying: .remote(url: url, checksum: checksum))
580+
default:
581+
throw InternalError("unknown checkout state \(kind)")
582+
}
583+
}
584+
585+
enum CodingKeys: CodingKey {
586+
case type
587+
case url
588+
case checksum
589+
}
590+
}
591+
}
592+
593+
struct PackageReference: Decodable {
594+
let identity: String
595+
let kind: String
596+
let location: String
597+
let name: String
598+
599+
init(from decoder: Decoder) throws {
600+
let container = try decoder.container(keyedBy: CodingKeys.self)
601+
self.identity = try container.decode(String.self, forKey: .identity)
602+
self.kind = try container.decode(String.self, forKey: .kind)
603+
self.name = try container.decode(String.self, forKey: .name)
604+
if let location = try container.decodeIfPresent(String.self, forKey: .location) {
605+
self.location = location
606+
} else if let path = try container.decodeIfPresent(String.self, forKey: .path) {
607+
self.location = path
608+
} else {
609+
throw StringError("invalid package ref, missing location and path")
610+
}
611+
}
612+
613+
enum CodingKeys: CodingKey {
614+
case identity
615+
case kind
616+
case location
617+
case path
618+
case name
619+
}
620+
}
621+
}
622+
}
623+
624+
extension Workspace.ManagedDependency {
625+
fileprivate init(_ dependency: WorkspaceStateStorage.V4.Dependency) throws {
626+
try self.init(
627+
packageRef: .init(dependency.packageRef),
628+
state: dependency.state.underlying,
629+
subpath: RelativePath(dependency.subpath)
630+
)
631+
}
632+
}
633+
634+
extension Workspace.ManagedArtifact {
635+
fileprivate init(_ artifact: WorkspaceStateStorage.V4.Artifact) throws {
636+
try self.init(
637+
packageRef: .init(artifact.packageRef),
638+
targetName: artifact.targetName,
639+
source: artifact.source.underlying,
640+
path: AbsolutePath(artifact.path)
641+
)
642+
}
643+
}
644+
645+
extension PackageModel.PackageReference {
646+
fileprivate init(_ reference: WorkspaceStateStorage.V4.PackageReference) throws {
647+
let identity = PackageIdentity.plain(reference.identity)
648+
let kind: PackageModel.PackageReference.Kind
649+
switch reference.kind {
650+
case "root":
651+
kind = try .root(.init(validating: reference.location))
652+
case "local":
653+
kind = try .fileSystem(.init(validating: reference.location))
654+
case "remote":
655+
if let path = try? AbsolutePath(validating: reference.location) {
656+
kind = .localSourceControl(path)
657+
} else if let url = URL(string: reference.location) {
658+
kind = .remoteSourceControl(url)
659+
} else {
660+
throw StringError("invalid package kind \(reference.kind)")
661+
}
662+
default:
663+
throw StringError("invalid package kind \(reference.kind)")
664+
}
665+
666+
self.init(
667+
identity: identity,
668+
kind: kind,
669+
name: reference.name
670+
)
671+
}
672+
}
673+
428674
extension CheckoutState {
429675
fileprivate init(_ state: WorkspaceStateStorage.V4.Dependency.State.CheckoutInfo) throws {
430676
let revision: Revision = .init(identifier: state.revision)

0 commit comments

Comments
 (0)