Skip to content

Commit c6cc52d

Browse files
IgorMuzykaAmzdmxcl
committed
Added support for @main attribute based scripts (#194)
* Added support for @main attribute based scripts, included async-main-count-lines Example * removed unnecessary manual #warning * Improved Package.swift manifest file creation * Minor improvements * Update Sources/Command/edit().swift Applying changes suggested by @Amzd for Swift <= 5.5 Co-authored-by: Casper Zandbergen <[email protected]> * Fix compilation in older Swifts --------- Co-authored-by: Casper Zandbergen <[email protected]> Co-authored-by: Max Howell <[email protected]>
1 parent 2b1e1fe commit c6cc52d

File tree

6 files changed

+86
-23
lines changed

6 files changed

+86
-23
lines changed

Examples/async-main-count-lines

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/user/bin/swift sh
2+
import Foundation
3+
import ArgumentParser // apple/swift-argument-parser ~> 1.4.0
4+
5+
/// example borrowed from [here](https://swiftpackageindex.com/apple/swift-argument-parser/1.4.0/documentation/argumentparser/asyncparsablecommand)
6+
@main
7+
struct CountLines: AsyncParsableCommand {
8+
@Argument(transform: URL.init(fileURLWithPath:))
9+
var inputFile: URL
10+
mutating func run() async throws {
11+
let fileHandle = try FileHandle(forReadingFrom: inputFile)
12+
let lineCount = try await fileHandle.bytes.lines.reduce(into: 0)
13+
{ count, _ in count += 1 }
14+
print(lineCount)
15+
}
16+
}

Sources/Command/edit().swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ import Path
77
public func edit(path: Path) throws -> Never {
88
#if os(macOS)
99
let input:Script.Input = .path(path)
10-
let deps = try StreamReader(path: path).compactMap { try ImportSpecification(line: $0, from: input) }
11-
let script = Script(for: .path(path), dependencies: deps)
10+
let reader = try StreamReader(path: path)
11+
var style: ExecutableTargetMainStyle = .topLevelCode
12+
let deps: [ImportSpecification] = try reader.compactMap { line in
13+
if line.contains("@main") && !(line.contains("//") || line.contains("/*")) {
14+
style = .mainAttribute
15+
}
16+
return try ImportSpecification(line: line, from: input)
17+
}
18+
let script = Script(for: .path(path), style: style, dependencies: deps)
1219
try script.write()
1320
try exec(arg0: "/usr/bin/swift", args: ["sh-edit", path.string, script.buildDirectory.string])
1421
#else

Sources/Command/editor().swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@ import Path
66

77
public func editor(path: Path) throws -> Never {
88
let input:Script.Input = .path(path)
9-
let deps = try StreamReader(path: path).compactMap { try ImportSpecification(line: $0, from: input) }
10-
let script = Script(for: .path(path), dependencies: deps)
9+
let reader = try StreamReader(path: path)
10+
var style: ExecutableTargetMainStyle = .topLevelCode
11+
let deps: [ImportSpecification] = try reader.compactMap { line in
12+
if line.contains("@main") && !(line.contains("//") || line.contains("/*")) {
13+
style = .mainAttribute
14+
}
15+
return try ImportSpecification(line: line, from: input)
16+
}
17+
let script = Script(for: .path(path), style: style, dependencies: deps)
1118
try script.write()
1219

1320
guard let editor = ProcessInfo.processInfo.environment["EDITOR"] else {

Sources/Command/run().swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private func run<T>(reader: StreamReader, input: Input, arguments: T) throws ->
2727

2828
var deps = [ImportSpecification]()
2929
var lines = [String]()
30+
var mainStyle: ExecutableTargetMainStyle = .topLevelCode
3031

3132
var transformedInput: Script.Input {
3233
switch input {
@@ -36,12 +37,14 @@ private func run<T>(reader: StreamReader, input: Input, arguments: T) throws ->
3637
return .path(path)
3738
}
3839
}
39-
4040
for (index, line) in reader.enumerated() {
4141
if index == 0, line.hasPrefix("#!") {
4242
lines.append("// shebang removed") // keep line numbers in sync
4343
continue
4444
}
45+
if line.contains("@main") && !(line.contains("//") || line.contains("/*")) {
46+
mainStyle = .mainAttribute
47+
}
4548
if let result = try ImportSpecification(line: line, from: transformedInput) {
4649
deps.append(result)
4750
}
@@ -53,7 +56,7 @@ private func run<T>(reader: StreamReader, input: Input, arguments: T) throws ->
5356
}
5457
}
5558

56-
let script = Script(for: transformedInput, dependencies: deps, arguments: Array(arguments))
59+
let script = Script(for: transformedInput, style: mainStyle, dependencies: deps, arguments: Array(arguments))
5760
try script.run()
5861
}
5962

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
/// wheter the script has @main
3+
public enum ExecutableTargetMainStyle {
4+
/// script has @main, script source file cannot be named main.swift, as this would be compilation error
5+
case mainAttribute
6+
/// script doesn't have @main, and it's ok to have main.swift file as script source
7+
case topLevelCode
8+
}

Sources/Script/Script.swift

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class Script {
88
let input: Input
99
let deps: [ImportSpecification]
1010
let args: [String]
11+
let mainStyle: ExecutableTargetMainStyle
1112

1213
private let inputPathHash: String?
1314

@@ -30,18 +31,22 @@ public class Script {
3031
}
3132

3233
public var mainSwift: Path {
33-
return buildDirectory/"main.swift"
34+
switch mainStyle {
35+
case .mainAttribute: return buildDirectory/"Root.swift"
36+
case .topLevelCode: return buildDirectory/"main.swift"
37+
}
3438
}
3539

3640
public enum Input {
3741
case path(Path)
3842
case string(name: String, content: String)
3943
}
4044

41-
public init(for: Input, dependencies: [ImportSpecification], arguments: [String] = []) {
45+
public init(for: Input, style: ExecutableTargetMainStyle, dependencies: [ImportSpecification], arguments: [String] = []) {
4246
input = `for`
4347
deps = dependencies
4448
args = arguments
49+
mainStyle = style
4550

4651
// cache hash if appropriate since accessed often and involves work
4752
if case let Input.path(path) = input {
@@ -68,20 +73,30 @@ public class Script {
6873
public func write() throws {
6974
//NOTE we only support Swift >= 4.2 basically
7075
//TODO dependency module names might not correspond the products that packages export, must parse `swift package dump-package` output
71-
7276
if depsCache != deps {
7377
// this check because SwiftPM has to reparse the manifest if we rewrite it
7478
// this is noticably slow, so avoid it if possible
75-
7679
var macOS: String {
7780
let version = ProcessInfo.processInfo.operatingSystemVersion
7881
return ".macOS(\"\(version.majorVersion).\(version.minorVersion)\")"
7982
}
80-
8183
try buildDirectory.mkdir(.p)
84+
let targetDefinition: String
85+
let swiftToolsVersion: String
86+
let sourceFile: String
87+
switch mainStyle {
88+
case .mainAttribute:
89+
targetDefinition = "executableTarget"
90+
swiftToolsVersion = "5.5"
91+
sourceFile = "Root.swift"
92+
case .topLevelCode:
93+
targetDefinition = "target"
94+
swiftToolsVersion = "5.1"
95+
sourceFile = "main.swift"
96+
}
8297
// we are using tools version 5.1 while we still can as >= 5.3 makes specifying deps significantly more complex
8398
try """
84-
// swift-tools-version:5.1
99+
// swift-tools-version:\(swiftToolsVersion)
85100
import PackageDescription
86101
87102
let pkg = Package(name: "\(name)")
@@ -93,12 +108,12 @@ public class Script {
93108
\(deps.packageLines)
94109
]
95110
pkg.targets = [
96-
.target(
111+
.\(targetDefinition)(
97112
name: "\(name)",
98113
dependencies: [\(deps.mainTargetDependencies)],
99114
path: ".",
100115
exclude: ["deps.json"],
101-
sources: ["main.swift"]
116+
sources: ["\(sourceFile)"]
102117
)
103118
]
104119
@@ -109,19 +124,26 @@ public class Script {
109124
#endif
110125
111126
""".write(to: manifestPath)
112-
113127
try JSONEncoder().encode(deps).write(to: depsCachePath)
114128
}
115-
116129
switch input {
117130
case .path(let userPath):
118131
func mklink() throws { try userPath.symlink(as: mainSwift) }
119-
120-
if let linkdst = try? mainSwift.readlink(), linkdst != userPath {
121-
try mainSwift.delete()
122-
try mklink()
123-
} else if !mainSwift.exists {
124-
try mklink()
132+
switch mainStyle {
133+
case .mainAttribute:
134+
try mainSwift.delete()
135+
let reader = try StreamReader(path: userPath)
136+
let source = reader.compactMap { line in
137+
line.contains("#!") ? .none : line
138+
}.joined(separator: "\n")
139+
try source.write(to: mainSwift)
140+
case .topLevelCode:
141+
if let linkdst = try? mainSwift.readlink(), linkdst != userPath {
142+
try mainSwift.delete()
143+
try mklink()
144+
} else if !mainSwift.exists {
145+
try mklink()
146+
}
125147
}
126148
case .string(_, let contents):
127149
if let currentContents = try? String(contentsOf: mainSwift), currentContents == contents { break }
@@ -178,12 +200,12 @@ public class Script {
178200
public func run() throws -> Never {
179201
if scriptChanged {
180202
try write()
181-
182203
// first arg has to be same as executable path
183204
let task = Process()
184205
task.launchPath = Path.swift.string
185206
task.arguments = ["build", "-Xswiftc", "-suppress-warnings"]
186207
task.currentDirectoryPath = buildDirectory.string
208+
187209
#if !os(Linux)
188210
task.standardOutput = task.standardError
189211
#else

0 commit comments

Comments
 (0)