Repository: hylo-lang/hylo (swift)

Issue:
Support `--version`
For applications like Compiler Explorer, getting a version identifier (even one produced by a git hash) is useful for triaging issues.

Pull request:
Closes https://github.com/hylo-lang/hylo/issues/1120

- Adds a `-V` / `--version` flag to the compiler.
- Adds a script to set the compiler version. This is mainly for use in CI so that tags can be set as the version before building. The script can also be called during local development to set the version to a git hash. It could help when triaging issues to understand the commit the compiler was built at. This was the motivation for the original [issue](https://github.com/hylo-lang/hylo/issues/1120).
- Updates CI to set the version of the compiler either to a tag (if a `v` prefixed tag is pushed), or the git hash in all other cases.

Patch:
diff --git a/Docs/CLI.md b/Docs/CLI.md
index 69b001733..5dab00048 100644
--- a/Docs/CLI.md
+++ b/Docs/CLI.md
@@ -94,3 +94,7 @@ Write output to `<file>`.
 ### `-v`, `--verbose`
 
 Use verbose output.
+
+### `-V`, `--version`
+
+Output the compiler version.
diff --git a/Sources/Driver/Driver.swift b/Sources/Driver/Driver.swift
index 347d71f70..902a5004a 100644
--- a/Sources/Driver/Driver.swift
+++ b/Sources/Driver/Driver.swift
@@ -10,6 +10,23 @@ import Utils
 
 public struct Driver: ParsableCommand {
 
+  /// A validation error that includes the command's full help message.
+  struct ValidationErrorWithHelp: Error, CustomStringConvertible {
+    var message: String
+
+    init(_ message: String) {
+      self.message = message
+    }
+
+    var description: String {
+      """
+      \(message)
+
+      \(Driver.helpMessage())
+      """
+    }
+  }
+
   /// The type of the output files to generate.
   private enum OutputType: String, ExpressibleByArgument {
 
@@ -107,6 +124,11 @@ public struct Driver: ParsableCommand {
     help: "Use verbose output.")
   private var verbose: Bool = false
 
+  @Flag(
+    name: [.customShort("V"), .long],
+    help: "Output the compiler version.")
+  private var version: Bool = false
+
   @Flag(
     name: [.customShort("O")],
     help: "Compile with optimizations.")
@@ -114,7 +136,7 @@ public struct Driver: ParsableCommand {
 
   @Argument(
     transform: URL.init(fileURLWithPath:))
-  private var inputs: [URL]
+  private var inputs: [URL] = []
 
   public init() {}
 
@@ -153,6 +175,15 @@ public struct Driver: ParsableCommand {
   /// Executes the command, accumulating diagnostics in `diagnostics`.
   private func executeCommand(diagnostics: inout DiagnosticSet) throws {
 
+    if version {
+      standardError.write("\(hcVersion)\n")
+      return
+    }
+
+    guard !inputs.isEmpty else {
+      throw ValidationErrorWithHelp("Missing expected argument '<inputs> ...'")
+    }
+
     if compileInputAsModules {
       fatalError("compilation as modules not yet implemented.")
     }
diff --git a/Sources/Driver/OutputFileHandle.swift b/Sources/Driver/OutputFileHandle.swift
index ce7a8c7eb..a8d568fe4 100644
--- a/Sources/Driver/OutputFileHandle.swift
+++ b/Sources/Driver/OutputFileHandle.swift
@@ -18,5 +18,5 @@ struct OutputFileHandle: TextOutputStream {
   }
 }
 
-/// An instance whose writes are difrected to the standard error stream.
+/// An instance whose writes are directed to the standard error stream.
 var standardError = OutputFileHandle(.standardError)
diff --git a/Sources/Driver/Version.swift b/Sources/Driver/Version.swift
new file mode 100644
index 000000000..5682e9e34
--- /dev/null
+++ b/Sources/Driver/Version.swift
@@ -0,0 +1,2 @@
+// DO NOT COMMIT CHANGES TO THIS FILE
+let hcVersion = "unknown"
diff --git a/Tools/set-hc-version.sh b/Tools/set-hc-version.sh
new file mode 100755
index 000000000..c8fa5efe0
--- /dev/null
+++ b/Tools/set-hc-version.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+# This script sets the version of the compiler
+# Pass the version as an argument, or omit the argument to set the version to the current HEAD git hash
+
+VERSION_FILE="Sources/Driver/Version.swift"
+
+if [ "$#" -ne 1 ]; then
+    version=$(git rev-parse HEAD)
+else
+    version=$1
+fi
+
+echo -e "// DO NOT COMMIT CHANGES TO THIS FILE\nlet hcVersion = \"${version}\"" > $VERSION_FILE
+
+# Ignore local changes to Version.swift
+git update-index --assume-unchanged $VERSION_FILE
+
+cat $VERSION_FILE


Test patch:
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index eeb3a70bf..cee47f51e 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -27,6 +27,8 @@ on:
       - "README.md"
       - "LICENSE"
       - ".gitignore"
+    tags:
+      - v*
   pull_request:
     branches: [ "**" ]
     paths-ignore:
@@ -37,7 +39,23 @@ on:
       - ".gitignore"
 
 jobs:
+  determine-version:
+    name: "Determine compiler version from git tag"
+    runs-on: ubuntu-latest
+    outputs:
+      version: ${{ steps.get-tag.outputs.version }}
+    steps:
+    - id: get-tag
+      run: |
+        if [[ $GITHUB_REF == refs/tags/* ]]; then
+          version=${GITHUB_REF#refs/tags/}
+        else
+          version=""
+        fi
+        echo "version=$version" >> $GITHUB_OUTPUT
+
   build-devcontainer:
+    needs: determine-version
     name: "Devcontainer: ${{ matrix.host.os }}/${{ matrix.configuration }}"
     strategy:
       fail-fast: false
@@ -63,6 +81,9 @@ jobs:
         submodules: true
         show-progress: false
 
+    - name: Set compiler version
+      run: ./Tools/set-hc-version.sh ${{ needs.determine-version.outputs.version }}
+
     - name: Build and Test
       uses: devcontainers/ci@v0.3
       with:
@@ -77,6 +98,7 @@ jobs:
               (grep -q "[.']EndToEndTests[/. ]test_" testoutput.txt && grep -q "[.']HyloTests[/. ]test_" testoutput.txt) ||
               (echo "error: generated tests failed to run; see
               https://github.com/apple/swift-package-manager/issues/6595" && false) )
+          .build/${{ matrix.configuration }}/hc --version
 
     - name: Check code coverage
       uses: mattpolzin/swift-codecov-action@0.7.3
@@ -86,6 +108,7 @@ jobs:
         CODECOV_JSON: .build/${{ matrix.configuration }}/codecov/*.json
 
   build-native-macos:
+    needs: determine-version
     name: "Native: ${{ matrix.host.os }}/${{ matrix.configuration }}"
     strategy:
       fail-fast: false
@@ -111,6 +134,9 @@ jobs:
         submodules: true
         show-progress: false
 
+    - name: Set compiler version
+      run: ./Tools/set-hc-version.sh ${{ needs.determine-version.outputs.version }}
+
     - name: Setup swift
       uses: swift-actions/setup-swift@v1
       with:
@@ -147,8 +173,12 @@ jobs:
             (grep -q "[.']EndToEndTests[/. ]test_" testoutput.txt && grep -q "[.']HyloTests[/. ]test_" testoutput.txt) ||
             (echo "error: generated tests failed to run; see
             https://github.com/apple/swift-package-manager/issues/6595" && false) )
-          
+
+    - name: Output compiler version
+      run: .build/${{ matrix.configuration }}/hc --version
+
   build-native-windows:
+    needs: determine-version
     name: "Native: windows-latest, debug"
     strategy:
       fail-fast: false
@@ -167,6 +197,10 @@ jobs:
         submodules: true
         show-progress: false
 
+    - name: Set compiler version
+      run: |
+        & "C:\Program Files\Git\bin\bash.exe" Tools/set-hc-version.sh ${{ needs.determine-version.outputs.version }}
+
     - name: Swift version
       run: swift --version
 
@@ -198,3 +232,6 @@ jobs:
 
     - name: Test
       run: swift test --parallel --skip-build --pkg-config-path . 
+
+    - name: Output compiler version
+      run: .build/debug/hc --version
diff --git a/Tests/DriverTests/DriverTests.swift b/Tests/DriverTests/DriverTests.swift
index f889d51ed..61929abb7 100644
--- a/Tests/DriverTests/DriverTests.swift
+++ b/Tests/DriverTests/DriverTests.swift
@@ -29,7 +29,8 @@ final class DriverTests: XCTestCase {
   }
 
   func testNoInput() throws {
-    XCTAssertThrowsError(try Driver.parse([]))
+    let cli = try Driver.parse([])
+    XCTAssertThrowsError(try cli.execute())
   }
 
   func testRawAST() throws {