From 20a679d588b879f739cc98ef64d034856ccbf49b Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 10:51:33 +0100 Subject: [PATCH 01/18] [no-ci] WIP Swift script to update Libsecp256k1 dependency --- .gitignore | 3 + UPDATE_DEPENDENCY.md | 116 +++++++ justfile | 10 + scripts/update-libsecp/Package.swift | 30 ++ .../Sources/UpdateLibsecp/main.swift | 320 ++++++++++++++++++ 5 files changed, 479 insertions(+) create mode 100644 UPDATE_DEPENDENCY.md create mode 100644 justfile create mode 100644 scripts/update-libsecp/Package.swift create mode 100644 scripts/update-libsecp/Sources/UpdateLibsecp/main.swift diff --git a/.gitignore b/.gitignore index bb460e7..5114e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +scripts/update-libsecp/Package.resolved +scripts/update-libsecp/.build +scripts/update-libsecp/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata \ No newline at end of file diff --git a/UPDATE_DEPENDENCY.md b/UPDATE_DEPENDENCY.md new file mode 100644 index 0000000..2a99a96 --- /dev/null +++ b/UPDATE_DEPENDENCY.md @@ -0,0 +1,116 @@ +# Update the libsecp256k1 submodule + +Standing in K1 project root, call it `K1_ROOT`. + +Find path to submodule (libsecp256k1) + +```sh +cat .gitmodules +``` + +Which will print something like: +``` +[submodule "Sources/secp256k1/libsecp256k1"] + path = Sources/secp256k1/libsecp256k1 + url = https://github.com/bitcoin-core/secp256k1.git +``` + +Read the value of `path` and save it into `DEPENDENCY_PATH` (`Sources/secp256k1/libsecp256k1`). + +## Get current (old) tag and commit +Read out the current tag and commit: +```sh +git submodule status | grep $DEPENDENCY_PATH +``` + +which will output something like +``` +1a53f4961f337b4d166c25fce72ef0dc88806618 Sources/secp256k1/libsecp256k1 (v0.7.1) +``` + +save it into `OLD_VERSION`. + +Extract the current (old) commit and tag +```sh +OLD_COMMIT=$(printf '%s\n' "$CURRENT_VERSION" | awk '{print $1}') +OLD_TAG=$(printf '%s\n' "$CURRENT_VERSION" | awk -F'[()]' '{print $2}') +``` + +```sh +cd $DEPENDENCY_PATH +``` + +Git fetch + +```sh +git fetch +``` + +Extract latest tag: +```sh +git describe --tags --abbrev=0 +``` +Call it `LATEST_TAG` + +Checkout latest tag + +```sh +git checkout $LATEST_TAG +``` + +Extract commit from tag +```sh +git rev-list -n 1 $LATEST_TAG +``` +call it `NEW_COMMIT` + +Go back to project root + +```sh +cd $K1_ROOT +``` + +Stage changes +```sh +git add $DEPENDENCY_PATH +``` + +Run tests +```sh +swift test +``` + +If and only if all tests passes, proceed + +## Update documented version in README.md + +in README.md replace old values of + +```text +> Current `libsecp256k1` version is [$OLD_TAG ($OLD_COMMIT)](https://github.com/bitcoin-core/secp256k1/releases/tag/$OLD_TAG) +``` + +with new version: +```text +> Current `libsecp256k1` version is [$LATEST_TAG ($NEW_COMMIT)](https://github.com/bitcoin-core/secp256k1/releases/tag/$LATEST_TAG) +``` + +Stage README.md changes: +```sh +git add README.md +``` + +Checkout new branch +```sh +git checkout -b bump/libsecp256k1_to_$LATEST_TAG +``` + +Commit changes +```sh +git commit -m "Update libsecp256k1 dependency to $LATEST_TAG ($NEW_COMMIT) [all unit tests passed]" +``` + +Push changes +```sh +git push --set-upstream origin $(git_current_branch) +``` \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..18aa67c --- /dev/null +++ b/justfile @@ -0,0 +1,10 @@ +default: test + +test: + swift test + +bump-dep dryRun="false": + swift run \ + --package-path scripts/update-libsecp \ + update-libsecp \ + {{ if dryRun == "true" { "--dry-run" } else { "" } }} \ No newline at end of file diff --git a/scripts/update-libsecp/Package.swift b/scripts/update-libsecp/Package.swift new file mode 100644 index 0000000..b2af3ba --- /dev/null +++ b/scripts/update-libsecp/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.2.3 + +import PackageDescription + +let package = Package( + name: "UpdateLibsecp", + platforms: [ + .macOS(.v13), + ], + products: [ + .executable( + name: "update-libsecp", + targets: ["UpdateLibsecp"] + ), + ], + dependencies: [ + .package( + url: "https://github.com/swiftlang/swift-subprocess", + from: "0.2.1" + ), + ], + targets: [ + .executableTarget( + name: "UpdateLibsecp", + dependencies: [ + .product(name: "Subprocess", package: "swift-subprocess"), + ] + ), + ] +) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift new file mode 100644 index 0000000..84d26ec --- /dev/null +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -0,0 +1,320 @@ +import Foundation +import Subprocess +import System + +// MARK: - UpdateLibsecpTool +@main +enum UpdateLibsecpTool { + static func main() async throws { + print("\n\n✨ Updating submodule libsecp256k1...") + let cli = try CLI.parse() + let projectRoot = try await cli.resolveRoot() + let dryRun = cli.dryRun + if dryRun { + print("🌵🏃‍♂️ Running in dry-run mode — we won't perform any changes.") + } else { + fatalError("only dry run supported for now") + } + + let dependencyPath = try readDependencyPath(in: projectRoot) + let dependencyFullPath = projectRoot.appending(dependencyPath) + + print("📦 Found libsecp256k1 submodule at \(dependencyPath)") + + let currentStatus = try await runCommand( + "git", + arguments: ["submodule", "status", "--", dependencyPath], + workingDirectory: projectRoot + ).stdout.trimmed() + + let old = try parseVersionLine(currentStatus) + print("🏷️ Current libsecp256k1 version: tag \(old.tag), commit \(old.commit)") + + print("🛜 Fetching latest tags in submodule…") + try await runCommand( + "git", + arguments: ["fetch"], + workingDirectory: dependencyFullPath + ) + + let latestTag = try await runCommand( + "git", + arguments: ["describe", "--tags", "--abbrev=0"], + workingDirectory: dependencyFullPath + ).stdout.trimmed() + print("🏷️🆕 Latest tag discovered: \(latestTag)") + + print("🏷️🔀 Checking out \(latestTag)…") + try await runCommand( + "git", + arguments: ["checkout", latestTag], + workingDirectory: dependencyFullPath + ) + + let newCommit = try await runCommand( + "git", + arguments: ["rev-list", "-n", "1", latestTag], + workingDirectory: dependencyFullPath + ).stdout.trimmed() + print("#️⃣🆕 New commit resolved from tag: \(newCommit)") + + print("➕📦 Staging submodule changes…") + try await runCommand( + "git", + arguments: ["add", dependencyPath], + workingDirectory: projectRoot + ) + + print("🧪 Running swift test…") + try await runCommand( + "swift", + arguments: ["test"], + workingDirectory: projectRoot + ) + print("🧪 Tests passed🏅.") + + print("📝 Updating README.md…") + try updateReadme( + root: projectRoot, + oldTag: old.tag, + oldCommit: old.commit, + newTag: latestTag, + newCommit: newCommit, + dryRun: dryRun + ) + + try await runCommand( + "git", + arguments: ["add", "README.md"], + workingDirectory: projectRoot + ) + + let branchName = "bump/libsecp256k1_to_\(latestTag)" + print("🪾🆕 Creating branch \(branchName)…") + try await runCommand( + "git", + arguments: ["checkout", "-b", branchName], + workingDirectory: projectRoot + ) + + let commitMessage = + "Update libsecp256k1 dependency to \(latestTag) (\(newCommit)) [all unit tests passed]" + print("💾 Committing changes…") + try await runCommand( + "git", + arguments: ["commit", "-m", commitMessage], + dryRun: dryRun, + workingDirectory: projectRoot + ) + + print("🛜🪾 Pushing branch to origin…") + try await runCommand( + "git", + arguments: ["push", "--set-upstream", "origin", branchName], + dryRun: dryRun, + workingDirectory: projectRoot + ) + + print("✅ Done!") + } +} + +// MARK: - CLI +private struct CLI { + let rootOverride: FilePath? + let cwd: FilePath + let dryRun: Bool + + static func parse() throws -> CLI { + var args = Array(CommandLine.arguments.dropFirst()) + var rootPath: FilePath? + var dryRun = false + + while let arg = args.first { + args.removeFirst() + switch arg { + case "--root": + guard let value = args.first else { + throw ToolError("Missing value for --root") + } + args.removeFirst() + rootPath = FilePath(value) + case "--dry-run": + dryRun = true + case "--help", "-h": + print( + """ + update-libsecp — updates the libsecp256k1 submodule to the latest tag. + + Options: + --root Override project root (defaults to repository root). + --dry-run Print actions without mutating the working copy. + -h, --help Show this help. + """) + exit(0) + default: + throw ToolError("Unrecognized argument: \(arg)") + } + } + + let cwd = FilePath(FileManager.default.currentDirectoryPath) + return CLI(rootOverride: rootPath, cwd: cwd, dryRun: dryRun) + } + + func resolveRoot() async throws -> FilePath { + if let override = rootOverride { + return override + } + + let result = try await run( + .name("git"), + arguments: ["rev-parse", "--show-toplevel"], + workingDirectory: cwd, + output: .string(limit: 4096), + error: .string(limit: 4096) + ) + + guard case .exited(0) = result.terminationStatus, + let path = result.standardOutput?.trimmed(), !path.isEmpty + else { + throw ToolError("Failed to determine git root from \(cwd)") + } + + return FilePath(path) + } +} + +// MARK: - Core helpers + +private func readDependencyPath(in root: FilePath) throws -> String { + let gitmodulesURL = URL(fileURLWithPath: root.appending(".gitmodules").description) + let contents = try String(contentsOf: gitmodulesURL) + + for rawLine in contents.split(whereSeparator: \.isNewline) { + let line = rawLine.trimmingCharacters(in: .whitespaces) + guard line.hasPrefix("path"), line.contains("libsecp256k1") else { continue } + let parts = line.split(separator: "=", maxSplits: 1).map { + $0.trimmingCharacters(in: .whitespaces) + } + if parts.count == 2 { + return parts[1] + } + } + + throw ToolError("Could not locate libsecp256k1 path inside .gitmodules") +} + +private func parseVersionLine(_ line: String) throws -> (commit: String, tag: String) { + let pattern = #"[-+ ]?([0-9a-f]{40})\s+[^\(]*\(([^)]+)\)"# + let regex = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(line.startIndex ..< line.endIndex, in: line) + guard let match = regex.firstMatch(in: line, options: [], range: range), + let commitRange = Range(match.range(at: 1), in: line), + let tagRange = Range(match.range(at: 2), in: line) + else { + throw ToolError("Unable to parse submodule status line: \(line)") + } + + return (String(line[commitRange]), String(line[tagRange])) +} + +private func updateReadme( + root: FilePath, + oldTag: String, + oldCommit: String, + newTag: String, + newCommit: String, + dryRun: Bool +) throws { + let readmeURL = URL(fileURLWithPath: root.appending("README.md").description) + let content = try String(contentsOf: readmeURL) + + let escapedTag = NSRegularExpression.escapedPattern(for: oldTag) + let escapedCommit = NSRegularExpression.escapedPattern(for: oldCommit) + let oldLinePattern = + "> Current `libsecp256k1` version is \\[\(escapedTag) \\(\(escapedCommit)\\)\\]\\([^\\n]*\\)" + + let regex = try NSRegularExpression(pattern: oldLinePattern, options: []) + let range = NSRange(location: 0, length: (content as NSString).length) + + let replacement = + "> Current `libsecp256k1` version is [\(newTag) (\(newCommit))](https://github.com/bitcoin-core/secp256k1/releases/tag/\(newTag))" + + let matches = regex.matches(in: content, options: [], range: range) + guard let match = matches.first else { + throw ToolError("Could not find README version line to replace.") + } + + if dryRun { + print("Skipped README update since dryRun...(but we successfully found the line to replace.)") + } else { + let updated = regex.stringByReplacingMatches( + in: content, + options: [], + range: match.range, + withTemplate: replacement + ) + try updated.write(to: readmeURL, atomically: true, encoding: String.Encoding.utf8) + } +} + +@discardableResult +private func runCommand( + _ executable: String, + arguments rawArgs: [String], + dryRun: Bool = false, + workingDirectory: FilePath +) async throws -> (stdout: String, stderr: String) { + var rawArgs = rawArgs + if dryRun { + rawArgs.append("--dry-run") + } + let result = try await run( + .name(executable), + arguments: Arguments(rawArgs), + workingDirectory: workingDirectory, + output: .string(limit: 1_000_000), + error: .string(limit: 1_000_000) + ) + + guard + case let .exited(code) = result.terminationStatus, + code == 0 + else { + let statusDescription: String + switch result.terminationStatus { + case let .exited(code): + statusDescription = "exit code \(code)" + case let .unhandledException(signal): + statusDescription = "terminated by signal \(signal)" + @unknown default: + statusDescription = "terminated for unknown reason" + } + + let failureSeparator = ", " + let failure = "\(executable) \(rawArgs.joined(separator: failureSeparator))" + throw ToolError( + """ + Command failed: \(failure) + Status: \(statusDescription) + Stderr: \(result.standardError ?? "") + Stdout: \(result.standardOutput ?? "") + """ + ) + } + + return (result.standardOutput ?? "", result.standardError ?? "") +} + +// MARK: - ToolError +private struct ToolError: LocalizedError { + let message: String + init(_ message: String) { self.message = message } + var errorDescription: String? { message } +} + +extension String { + fileprivate func trimmed() -> String { + trimmingCharacters(in: .whitespacesAndNewlines) + } +} From 8042fe1ae46f35e075810813df9b352f8445ec36 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 10:55:45 +0100 Subject: [PATCH 02/18] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c46b1c..b4e60da 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ _K1_ is Swift wrapper around [libsecp256k1 (bitcoin-core/secp256k1)][lib], offering ECDSA, Schnorr ([BIP340][bip340]) and ECDH features. > [!NOTE] -> Current `libsecp256k1` version is [0.6.0 (0cdc758a56360bf58a851fe91085a327ec97685a)](https://github.com/bitcoin-core/secp256k1/commit/0cdc758a56360bf58a851fe91085a327ec97685a) +> Current `libsecp256k1` version is [v0.6.0 (0cdc758a56360bf58a851fe91085a327ec97685a)](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.6.0) # Documentation Read full documentation [here on SwiftPackageIndex][doc]. From 75838ec9caaa7c77c3b5ece2a05afe450a89f15a Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 11:13:55 +0100 Subject: [PATCH 03/18] WIP --- UPDATE_DEPENDENCY.md | 6 ++--- .../Sources/UpdateLibsecp/main.swift | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/UPDATE_DEPENDENCY.md b/UPDATE_DEPENDENCY.md index 2a99a96..6e34bac 100644 --- a/UPDATE_DEPENDENCY.md +++ b/UPDATE_DEPENDENCY.md @@ -46,9 +46,9 @@ Git fetch git fetch ``` -Extract latest tag: +Extract tag with highest semver number (somewhat incorrectly we call it "latest"): ```sh -git describe --tags --abbrev=0 +git tag --list 'v*' --sort=-v:refname | head -n 1 ``` Call it `LATEST_TAG` @@ -113,4 +113,4 @@ git commit -m "Update libsecp256k1 dependency to $LATEST_TAG ($NEW_COMMIT) [all Push changes ```sh git push --set-upstream origin $(git_current_branch) -``` \ No newline at end of file +``` diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 84d26ec..2996495 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -37,11 +37,11 @@ enum UpdateLibsecpTool { workingDirectory: dependencyFullPath ) - let latestTag = try await runCommand( - "git", - arguments: ["describe", "--tags", "--abbrev=0"], + let latestTag = try await firstLineOf( + command: "git", + arguments: ["tag", "--list", "v*", "--sort=-v:refname"], workingDirectory: dependencyFullPath - ).stdout.trimmed() + ) print("🏷️🆕 Latest tag discovered: \(latestTag)") print("🏷️🔀 Checking out \(latestTag)…") @@ -258,6 +258,25 @@ private func updateReadme( } } +@discardableResult +private func firstLineOf( + command executable: String, + arguments rawArgs: [String], + dryRun: Bool = false, + workingDirectory: FilePath +) async throws -> String { + let (stdout, stderr) = try await runCommand( + executable, + arguments: rawArgs, + dryRun: dryRun, + workingDirectory: workingDirectory + ) + guard let firstLine = stdout.split(separator: "\n").first else { + throw ToolError("No first line returned from command. Output was: \(stdout), stderr: \(stderr)") + } + return String(firstLine) +} + @discardableResult private func runCommand( _ executable: String, From 70bc88809c1b1fa94fccc31090a53603eb2f0db6 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 11:29:17 +0100 Subject: [PATCH 04/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 2996495..7854f9a 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -5,6 +5,9 @@ import System // MARK: - UpdateLibsecpTool @main enum UpdateLibsecpTool { + // TODO: we can use `git submodule update -- Sources/secp256k1/libsecp256k1` to "reset"/"undo" the + // checking out of libsecp256k1 submodule to its newer commit. Figure out if we wanna do that + // if `dryRun` is set, since it should not change any state... static func main() async throws { print("\n\n✨ Updating submodule libsecp256k1...") let cli = try CLI.parse() @@ -86,16 +89,28 @@ enum UpdateLibsecpTool { try await runCommand( "git", arguments: ["add", "README.md"], + dryRun: dryRun, workingDirectory: projectRoot ) - let branchName = "bump/libsecp256k1_to_\(latestTag)" - print("🪾🆕 Creating branch \(branchName)…") - try await runCommand( - "git", - arguments: ["checkout", "-b", branchName], - workingDirectory: projectRoot - ) + let branchName: String + if dryRun { + let currentBranch = try await firstLineOf( + command: "git", + arguments: ["branch", "--show-current"], + workingDirectory: projectRoot + ).trimmed() + branchName = currentBranch + } else { + let newBranch = "bump/libsecp256k1_to_\(latestTag)" + print("🪾🆕 Creating branch \(newBranch)…") + try await runCommand( + "git", + arguments: ["checkout", "-b", newBranch], + workingDirectory: projectRoot + ) + branchName = newBranch + } let commitMessage = "Update libsecp256k1 dependency to \(latestTag) (\(newCommit)) [all unit tests passed]" @@ -288,13 +303,15 @@ private func runCommand( if dryRun { rawArgs.append("--dry-run") } + let arguments = Arguments(rawArgs) let result = try await run( .name(executable), - arguments: Arguments(rawArgs), + arguments: arguments, workingDirectory: workingDirectory, output: .string(limit: 1_000_000), error: .string(limit: 1_000_000) ) + print("DEBUG: args \(arguments)") guard case let .exited(code) = result.terminationStatus, @@ -311,7 +328,7 @@ private func runCommand( } let failureSeparator = ", " - let failure = "\(executable) \(rawArgs.joined(separator: failureSeparator))" + let failure = "\(executable) \(arguments)" throw ToolError( """ Command failed: \(failure) @@ -322,6 +339,8 @@ private func runCommand( ) } + print("DEBUG: std out \(result.standardOutput)") + return (result.standardOutput ?? "", result.standardError ?? "") } From 33f58f85a3734150dc75baeeda8bfe93dbb8bddc Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 11:47:53 +0100 Subject: [PATCH 05/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 7854f9a..f745fa3 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -5,9 +5,6 @@ import System // MARK: - UpdateLibsecpTool @main enum UpdateLibsecpTool { - // TODO: we can use `git submodule update -- Sources/secp256k1/libsecp256k1` to "reset"/"undo" the - // checking out of libsecp256k1 submodule to its newer commit. Figure out if we wanna do that - // if `dryRun` is set, since it should not change any state... static func main() async throws { print("\n\n✨ Updating submodule libsecp256k1...") let cli = try CLI.parse() @@ -30,6 +27,12 @@ enum UpdateLibsecpTool { workingDirectory: projectRoot ).stdout.trimmed() + let currentBranch = try await firstLineOf( + command: "git", + arguments: ["branch", "--show-current"], + workingDirectory: projectRoot + ).trimmed() + let old = try parseVersionLine(currentStatus) print("🏷️ Current libsecp256k1 version: tag \(old.tag), commit \(old.commit)") @@ -54,6 +57,31 @@ enum UpdateLibsecpTool { workingDirectory: dependencyFullPath ) + defer { + // Reset submodule change if dry run + if dryRun { + do { + try await runCommand( + "git", + arguments: ["submodule", "update", "--", dependencyPath], + workingDirectory: dependencyFullPath + ) + } catch { + print("❌ Error while resetting submodule changes: \(error)") + } + } + // Switch back to working branch + do { + try await runCommand( + "git", + arguments: ["switch", currentBranch], + workingDirectory: dependencyFullPath + ) + } catch { + print("❌ Error while switching back to branch '\(currentBranch)': \(error)") + } + } + let newCommit = try await runCommand( "git", arguments: ["rev-list", "-n", "1", latestTag], @@ -95,11 +123,6 @@ enum UpdateLibsecpTool { let branchName: String if dryRun { - let currentBranch = try await firstLineOf( - command: "git", - arguments: ["branch", "--show-current"], - workingDirectory: projectRoot - ).trimmed() branchName = currentBranch } else { let newBranch = "bump/libsecp256k1_to_\(latestTag)" From 65a47bfe9f8f0c5f34c89467c0357234f86064f2 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 14:21:19 +0100 Subject: [PATCH 06/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 99 ++++++++++++++----- 1 file changed, 73 insertions(+), 26 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index f745fa3..bc7cef7 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -8,6 +8,24 @@ enum UpdateLibsecpTool { static func main() async throws { print("\n\n✨ Updating submodule libsecp256k1...") let cli = try CLI.parse() + let program = try await Program.from(cli: cli) + try await program.run() + } +} + +// MARK: - Program +struct Program { + let dryRun: Bool + let projectRoot: FilePath + let dependencyPath: String +} + +extension Program { + var dependencyFullPath: FilePath { + projectRoot.appending(dependencyPath) + } + + fileprivate static func from(cli: CLI) async throws -> Self { let projectRoot = try await cli.resolveRoot() let dryRun = cli.dryRun if dryRun { @@ -17,10 +35,12 @@ enum UpdateLibsecpTool { } let dependencyPath = try readDependencyPath(in: projectRoot) - let dependencyFullPath = projectRoot.appending(dependencyPath) - print("📦 Found libsecp256k1 submodule at \(dependencyPath)") + return Self(dryRun: dryRun, projectRoot: projectRoot, dependencyPath: dependencyPath) + } + func run() async throws { + print("🏃‍♂️📦 running with args: \(self)") let currentStatus = try await runCommand( "git", arguments: ["submodule", "status", "--", dependencyPath], @@ -39,7 +59,7 @@ enum UpdateLibsecpTool { print("🛜 Fetching latest tags in submodule…") try await runCommand( "git", - arguments: ["fetch"], + arguments: ["fetch", "--tags", "origin"], workingDirectory: dependencyFullPath ) @@ -57,31 +77,62 @@ enum UpdateLibsecpTool { workingDirectory: dependencyFullPath ) - defer { - // Reset submodule change if dry run - if dryRun { - do { - try await runCommand( - "git", - arguments: ["submodule", "update", "--", dependencyPath], - workingDirectory: dependencyFullPath - ) - } catch { - print("❌ Error while resetting submodule changes: \(error)") - } - } - // Switch back to working branch + do { + try await proceed( + latestTag: latestTag, + currentBranch: currentBranch, + old: old + ) + + await cleanUp( + currentBranch: currentBranch, + ) + } catch { + await cleanUp( + currentBranch: currentBranch, + error: error + ) + } + + print("✅ Done!") + } + + func cleanUp( + currentBranch: String, + error originalError: Swift.Error? = nil + ) async { + if let originalError { + print("Cleaning up due to error: \(originalError)") + } + // Reset submodule change if dry run + if dryRun { do { try await runCommand( "git", - arguments: ["switch", currentBranch], + arguments: ["submodule", "update", "--", dependencyPath], workingDirectory: dependencyFullPath ) } catch { - print("❌ Error while switching back to branch '\(currentBranch)': \(error)") + print("❌ Error while resetting submodule changes: \(error)") } } + // Switch back to working branch + do { + try await runCommand( + "git", + arguments: ["switch", currentBranch], + workingDirectory: dependencyFullPath + ) + } catch { + print("❌ Error while switching back to branch '\(currentBranch)': \(error)") + } + } + func proceed( + latestTag: String, + currentBranch: String, + old: (tag: String, commit: String), + ) async throws { let newCommit = try await runCommand( "git", arguments: ["rev-list", "-n", "1", latestTag], @@ -152,8 +203,6 @@ enum UpdateLibsecpTool { dryRun: dryRun, workingDirectory: projectRoot ) - - print("✅ Done!") } } @@ -280,7 +329,7 @@ private func updateReadme( let matches = regex.matches(in: content, options: [], range: range) guard let match = matches.first else { - throw ToolError("Could not find README version line to replace.") + throw ToolError("Could not find README version line to replace, searched for line:\n\(oldLinePattern)") } if dryRun { @@ -303,6 +352,7 @@ private func firstLineOf( dryRun: Bool = false, workingDirectory: FilePath ) async throws -> String { + print("rawArgs", rawArgs) let (stdout, stderr) = try await runCommand( executable, arguments: rawArgs, @@ -310,7 +360,7 @@ private func firstLineOf( workingDirectory: workingDirectory ) guard let firstLine = stdout.split(separator: "\n").first else { - throw ToolError("No first line returned from command. Output was: \(stdout), stderr: \(stderr)") + throw ToolError("No first line returned from command. Output was: '\(stdout)', stderr: '\(stderr)'") } return String(firstLine) } @@ -334,7 +384,6 @@ private func runCommand( output: .string(limit: 1_000_000), error: .string(limit: 1_000_000) ) - print("DEBUG: args \(arguments)") guard case let .exited(code) = result.terminationStatus, @@ -362,8 +411,6 @@ private func runCommand( ) } - print("DEBUG: std out \(result.standardOutput)") - return (result.standardOutput ?? "", result.standardError ?? "") } From fb76123fb734d979521db83cb9faff287511ab21 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 14:32:03 +0100 Subject: [PATCH 07/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 126 ++++++++++-------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index bc7cef7..5edf1b3 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -53,8 +53,8 @@ extension Program { workingDirectory: projectRoot ).trimmed() - let old = try parseVersionLine(currentStatus) - print("🏷️ Current libsecp256k1 version: tag \(old.tag), commit \(old.commit)") + let oldVersion = try parseVersionLine(currentStatus) + print("🏷️ Current libsecp256k1 version: \(oldVersion)") print("🛜 Fetching latest tags in submodule…") try await runCommand( @@ -81,7 +81,7 @@ extension Program { try await proceed( latestTag: latestTag, currentBranch: currentBranch, - old: old + oldVersion: oldVersion ) await cleanUp( @@ -131,7 +131,7 @@ extension Program { func proceed( latestTag: String, currentBranch: String, - old: (tag: String, commit: String), + oldVersion: Version ) async throws { let newCommit = try await runCommand( "git", @@ -140,12 +140,16 @@ extension Program { ).stdout.trimmed() print("#️⃣🆕 New commit resolved from tag: \(newCommit)") - print("➕📦 Staging submodule changes…") - try await runCommand( - "git", - arguments: ["add", dependencyPath], - workingDirectory: projectRoot - ) + if dryRun { + print("➕🌵 Skipping git add of submodole changes since dry run.") + } else { + print("➕📦 Staging submodule changes…") + try await runCommand( + "git", + arguments: ["add", dependencyPath], + workingDirectory: projectRoot + ) + } print("🧪 Running swift test…") try await runCommand( @@ -156,13 +160,10 @@ extension Program { print("🧪 Tests passed🏅.") print("📝 Updating README.md…") + let newVersion = Version(tag: latestTag, commit: newCommit) try updateReadme( - root: projectRoot, - oldTag: old.tag, - oldCommit: old.commit, - newTag: latestTag, - newCommit: newCommit, - dryRun: dryRun + oldVersion: oldVersion, + newVersion: newVersion ) try await runCommand( @@ -291,57 +292,67 @@ private func readDependencyPath(in root: FilePath) throws -> String { throw ToolError("Could not locate libsecp256k1 path inside .gitmodules") } -private func parseVersionLine(_ line: String) throws -> (commit: String, tag: String) { +private func parseVersionLine(_ line: String) throws -> Version { let pattern = #"[-+ ]?([0-9a-f]{40})\s+[^\(]*\(([^)]+)\)"# let regex = try NSRegularExpression(pattern: pattern, options: []) let range = NSRange(line.startIndex ..< line.endIndex, in: line) - guard let match = regex.firstMatch(in: line, options: [], range: range), - let commitRange = Range(match.range(at: 1), in: line), - let tagRange = Range(match.range(at: 2), in: line) + + guard + let match = regex.firstMatch(in: line, options: [], range: range), + let commitRange = Range(match.range(at: 1), in: line), + let tagRange = Range(match.range(at: 2), in: line) else { throw ToolError("Unable to parse submodule status line: \(line)") } - return (String(line[commitRange]), String(line[tagRange])) + return Version( + tag: .init(line[tagRange]), + commit: .init(line[commitRange]) + ) } -private func updateReadme( - root: FilePath, - oldTag: String, - oldCommit: String, - newTag: String, - newCommit: String, - dryRun: Bool -) throws { - let readmeURL = URL(fileURLWithPath: root.appending("README.md").description) - let content = try String(contentsOf: readmeURL) - - let escapedTag = NSRegularExpression.escapedPattern(for: oldTag) - let escapedCommit = NSRegularExpression.escapedPattern(for: oldCommit) - let oldLinePattern = - "> Current `libsecp256k1` version is \\[\(escapedTag) \\(\(escapedCommit)\\)\\]\\([^\\n]*\\)" - - let regex = try NSRegularExpression(pattern: oldLinePattern, options: []) - let range = NSRange(location: 0, length: (content as NSString).length) - - let replacement = - "> Current `libsecp256k1` version is [\(newTag) (\(newCommit))](https://github.com/bitcoin-core/secp256k1/releases/tag/\(newTag))" - - let matches = regex.matches(in: content, options: [], range: range) - guard let match = matches.first else { - throw ToolError("Could not find README version line to replace, searched for line:\n\(oldLinePattern)") - } +// MARK: - Version +struct Version { + let tag: String + let commit: String +} - if dryRun { - print("Skipped README update since dryRun...(but we successfully found the line to replace.)") - } else { - let updated = regex.stringByReplacingMatches( - in: content, - options: [], - range: match.range, - withTemplate: replacement - ) - try updated.write(to: readmeURL, atomically: true, encoding: String.Encoding.utf8) +extension Program { + private func updateReadme( + oldVersion: Version, + newVersion: Version + ) throws { + let readmeURL = URL(fileURLWithPath: projectRoot.appending("README.md").description) + let content = try String(contentsOf: readmeURL) + + let escapedTag = NSRegularExpression.escapedPattern(for: oldVersion.tag) + let escapedCommit = NSRegularExpression.escapedPattern(for: oldVersion.commit) + let newTag = newVersion.tag + let oldLinePattern = + "> Current `libsecp256k1` version is \\[\(escapedTag) \\(\(escapedCommit)\\)\\]\\([^\\n]*\\)" + + let regex = try NSRegularExpression(pattern: oldLinePattern, options: []) + let range = NSRange(location: 0, length: (content as NSString).length) + + let replacement = + "> Current `libsecp256k1` version is [\(newTag) (\(newVersion.commit))](https://github.com/bitcoin-core/secp256k1/releases/tag/\(newTag))" + + let matches = regex.matches(in: content, options: [], range: range) + guard let match = matches.first else { + throw ToolError("Could not find README version line to replace, searched for line:\n\(oldLinePattern)") + } + + if dryRun { + print("Skipped README update since dryRun...(but we successfully found the line to replace.)") + } else { + let updated = regex.stringByReplacingMatches( + in: content, + options: [], + range: match.range, + withTemplate: replacement + ) + try updated.write(to: readmeURL, atomically: true, encoding: .utf8) + } } } @@ -399,7 +410,6 @@ private func runCommand( statusDescription = "terminated for unknown reason" } - let failureSeparator = ", " let failure = "\(executable) \(arguments)" throw ToolError( """ From e9a71b78f2a1c736637815c4382c075fe959dff9 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 14:33:22 +0100 Subject: [PATCH 08/18] WIP --- scripts/update-libsecp/Sources/UpdateLibsecp/main.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 5edf1b3..7de9636 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -70,6 +70,11 @@ extension Program { ) print("🏷️🆕 Latest tag discovered: \(latestTag)") + if oldVersion == latestTag { + print("Current version == latest tag — nothing to update. Exiting ✅.") + return + } + print("🏷️🔀 Checking out \(latestTag)…") try await runCommand( "git", @@ -312,7 +317,7 @@ private func parseVersionLine(_ line: String) throws -> Version { } // MARK: - Version -struct Version { +struct Version: Equatable { let tag: String let commit: String } From 94016f6dc6aada5959d70cdd0d9e36d23849d202 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 14:49:53 +0100 Subject: [PATCH 09/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 82 ++++++++++++------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 7de9636..94d4768 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -39,53 +39,81 @@ extension Program { return Self(dryRun: dryRun, projectRoot: projectRoot, dependencyPath: dependencyPath) } - func run() async throws { - print("🏃‍♂️📦 running with args: \(self)") - let currentStatus = try await runCommand( + func submoduleStatus() async throws -> String { + try await runCommand( "git", arguments: ["submodule", "status", "--", dependencyPath], workingDirectory: projectRoot ).stdout.trimmed() + } - let currentBranch = try await firstLineOf( + func currentBranch() async throws -> String { + try await firstLineOf( command: "git", arguments: ["branch", "--show-current"], workingDirectory: projectRoot ).trimmed() + } - let oldVersion = try parseVersionLine(currentStatus) - print("🏷️ Current libsecp256k1 version: \(oldVersion)") + func currentVersion() async throws -> Version { + let currentStatus = try await submoduleStatus() + return try parseVersionLine(currentStatus) + } - print("🛜 Fetching latest tags in submodule…") + func fetchLatestTags() async throws { try await runCommand( "git", arguments: ["fetch", "--tags", "origin"], workingDirectory: dependencyFullPath ) + } - let latestTag = try await firstLineOf( + func getLatestTag() async throws -> String { + try await fetchLatestTags() + return try await firstLineOf( command: "git", arguments: ["tag", "--list", "v*", "--sort=-v:refname"], workingDirectory: dependencyFullPath ) + } + + func checkout(tag: String) async throws -> Version { + try await runCommand( + "git", + arguments: ["checkout", tag], + workingDirectory: dependencyFullPath + ) + let commit = try await runCommand( + "git", + arguments: ["rev-list", "-n", "1", tag], + workingDirectory: dependencyFullPath + ).stdout.trimmed() + print("#️⃣🆕 Commit resolved from tag: \(commit)") + return Version(tag: tag, commit: commit) + } + + func run() async throws { + let currentBranch = try await currentBranch() + + let oldVersion = try await currentVersion() + print("🏷️ Current libsecp256k1 version: \(oldVersion)") + + print("🛜 Fetching latest tags in submodule…") + let latestTag = try await getLatestTag() print("🏷️🆕 Latest tag discovered: \(latestTag)") - if oldVersion == latestTag { + if oldVersion.tag == latestTag { print("Current version == latest tag — nothing to update. Exiting ✅.") return } print("🏷️🔀 Checking out \(latestTag)…") - try await runCommand( - "git", - arguments: ["checkout", latestTag], - workingDirectory: dependencyFullPath - ) + let latestVersion = try await checkout(tag: latestTag) do { try await proceed( - latestTag: latestTag, - currentBranch: currentBranch, + branchAtStart: currentBranch, + latestVersion: latestVersion, oldVersion: oldVersion ) @@ -134,19 +162,12 @@ extension Program { } func proceed( - latestTag: String, - currentBranch: String, + branchAtStart: String, + latestVersion newVersion: Version, oldVersion: Version ) async throws { - let newCommit = try await runCommand( - "git", - arguments: ["rev-list", "-n", "1", latestTag], - workingDirectory: dependencyFullPath - ).stdout.trimmed() - print("#️⃣🆕 New commit resolved from tag: \(newCommit)") - if dryRun { - print("➕🌵 Skipping git add of submodole changes since dry run.") + print("➕🌵 Skipping git add of submodule changes since dry run.") } else { print("➕📦 Staging submodule changes…") try await runCommand( @@ -165,7 +186,7 @@ extension Program { print("🧪 Tests passed🏅.") print("📝 Updating README.md…") - let newVersion = Version(tag: latestTag, commit: newCommit) + try updateReadme( oldVersion: oldVersion, newVersion: newVersion @@ -180,9 +201,9 @@ extension Program { let branchName: String if dryRun { - branchName = currentBranch + branchName = branchAtStart } else { - let newBranch = "bump/libsecp256k1_to_\(latestTag)" + let newBranch = "bump/libsecp256k1_to_\(newVersion.tag)" print("🪾🆕 Creating branch \(newBranch)…") try await runCommand( "git", @@ -193,7 +214,7 @@ extension Program { } let commitMessage = - "Update libsecp256k1 dependency to \(latestTag) (\(newCommit)) [all unit tests passed]" + "Update libsecp256k1 dependency to \(newVersion) [all unit tests passed]" print("💾 Committing changes…") try await runCommand( "git", @@ -368,7 +389,6 @@ private func firstLineOf( dryRun: Bool = false, workingDirectory: FilePath ) async throws -> String { - print("rawArgs", rawArgs) let (stdout, stderr) = try await runCommand( executable, arguments: rawArgs, From 16f2188e837b401271966d84b7d648986b1e0ac3 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:38:13 +0100 Subject: [PATCH 10/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 275 +++++++++++------- 1 file changed, 176 insertions(+), 99 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 94d4768..6b509ce 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -39,75 +39,16 @@ extension Program { return Self(dryRun: dryRun, projectRoot: projectRoot, dependencyPath: dependencyPath) } - func submoduleStatus() async throws -> String { - try await runCommand( - "git", - arguments: ["submodule", "status", "--", dependencyPath], - workingDirectory: projectRoot - ).stdout.trimmed() - } - - func currentBranch() async throws -> String { - try await firstLineOf( - command: "git", - arguments: ["branch", "--show-current"], - workingDirectory: projectRoot - ).trimmed() - } - - func currentVersion() async throws -> Version { - let currentStatus = try await submoduleStatus() - return try parseVersionLine(currentStatus) - } - - func fetchLatestTags() async throws { - try await runCommand( - "git", - arguments: ["fetch", "--tags", "origin"], - workingDirectory: dependencyFullPath - ) - } - - func getLatestTag() async throws -> String { - try await fetchLatestTags() - return try await firstLineOf( - command: "git", - arguments: ["tag", "--list", "v*", "--sort=-v:refname"], - workingDirectory: dependencyFullPath - ) - } - - func checkout(tag: String) async throws -> Version { - try await runCommand( - "git", - arguments: ["checkout", tag], - workingDirectory: dependencyFullPath - ) - let commit = try await runCommand( - "git", - arguments: ["rev-list", "-n", "1", tag], - workingDirectory: dependencyFullPath - ).stdout.trimmed() - print("#️⃣🆕 Commit resolved from tag: \(commit)") - return Version(tag: tag, commit: commit) - } - func run() async throws { let currentBranch = try await currentBranch() - - let oldVersion = try await currentVersion() - print("🏷️ Current libsecp256k1 version: \(oldVersion)") - - print("🛜 Fetching latest tags in submodule…") + let oldVersion = try await getCurrentVersion() let latestTag = try await getLatestTag() - print("🏷️🆕 Latest tag discovered: \(latestTag)") if oldVersion.tag == latestTag { print("Current version == latest tag — nothing to update. Exiting ✅.") return } - print("🏷️🔀 Checking out \(latestTag)…") let latestVersion = try await checkout(tag: latestTag) do { @@ -130,6 +71,30 @@ extension Program { print("✅ Done!") } + func proceed( + branchAtStart: String, + latestVersion newVersion: Version, + oldVersion: Version + ) async throws { + try await stageSubmoduleChangesIfLive() + try await test() + + try updateReadme( + oldVersion: oldVersion, + newVersion: newVersion + ) + + guard !dryRun else { + print("dryRun: skipping git commands: [add README, checkout branch, commit, push]") + return + } + + try await stageReadme() + let newBranch = try await checkoutNewBranch() + try await commitChanges(newVersion: newVersion) + try await push(branch: newBranch) + } + func cleanUp( currentBranch: String, error originalError: Swift.Error? = nil @@ -160,77 +125,176 @@ extension Program { print("❌ Error while switching back to branch '\(currentBranch)': \(error)") } } +} - func proceed( - branchAtStart: String, - latestVersion newVersion: Version, - oldVersion: Version - ) async throws { - if dryRun { - print("➕🌵 Skipping git add of submodule changes since dry run.") - } else { - print("➕📦 Staging submodule changes…") - try await runCommand( - "git", - arguments: ["add", dependencyPath], - workingDirectory: projectRoot - ) - } - +// MARK: Helper Methods +extension Program { + func test() async throws { print("🧪 Running swift test…") try await runCommand( "swift", arguments: ["test"], workingDirectory: projectRoot ) - print("🧪 Tests passed🏅.") + print("🧪 Tests passed ☑️.") + } - print("📝 Updating README.md…") + func submoduleStatus() async throws -> String { + try await runCommand( + "git", + arguments: ["submodule", "status", "--", dependencyPath], + workingDirectory: projectRoot + ).stdout.trimmed() + } - try updateReadme( - oldVersion: oldVersion, - newVersion: newVersion + func currentBranch() async throws -> String { + try await firstLineOf( + command: "git", + arguments: ["branch", "--show-current"], + workingDirectory: projectRoot + ).trimmed() + } + + func getCurrentVersion() async throws -> Version { + print("🏷️ Getting current libsecp256k1 version: \(oldVersion)") + let oldVersion = try await doGetCurrentVersion() + print("🏷️ Got current libsecp256k1 version: \(oldVersion)") + return oldVersion + } + + func doGetCurrentVersion() async throws -> Version { + let currentStatus = try await submoduleStatus() + return try parseVersionLine(currentStatus) + } + + func fetchLatestTags() async throws { + try await runCommand( + "git", + arguments: ["fetch", "--tags", "origin"], + workingDirectory: dependencyFullPath + ) + } + + func getLatestTag() async throws -> String { + print("🛜 Fetching latest tags in submodule…") + let latestTag = try await doGetLatestTag() + print("🛜 Fetched latest tag in submodule: \(latestTag) ☑️.") + return latestTag + } + + func doGetLatestTag() async throws -> String { + try await fetchLatestTags() + return try await firstLineOf( + command: "git", + arguments: ["tag", "--list", "v*", "--sort=-v:refname"], + workingDirectory: dependencyFullPath ) + } + func checkout(tag: String) async throws -> Version { + print("🏷️🔀 Checking out \(latestTag)…") + let latestVersion = try await doCheckout(tag: latestTag) + print("🏷️🔀 Checked out \(latestTag) ☑️.") + return latestVersion + } + + func doCheckout(tag: String) async throws -> Version { + try await runCommand( + "git", + arguments: ["checkout", tag], + workingDirectory: dependencyFullPath + ) + let commit = try await runCommand( + "git", + arguments: ["rev-list", "-n", "1", tag], + workingDirectory: dependencyFullPath + ).stdout.trimmed() + print("#️⃣🆕 Commit resolved from tag: \(commit)") + return Version(tag: tag, commit: commit) + } + + func stageReadme() async throws { + print("➕📄 Staging README change…") + try await stageReadme() + print("➕📄 Staged README change ☑️.") + } + + func doStageReadme() async throws { try await runCommand( "git", arguments: ["add", "README.md"], - dryRun: dryRun, workingDirectory: projectRoot ) + } - let branchName: String - if dryRun { - branchName = branchAtStart - } else { - let newBranch = "bump/libsecp256k1_to_\(newVersion.tag)" - print("🪾🆕 Creating branch \(newBranch)…") - try await runCommand( - "git", - arguments: ["checkout", "-b", newBranch], - workingDirectory: projectRoot - ) - branchName = newBranch - } + func checkoutNewBranch() async throws -> String { + print("🪾🆕 Checked out new branch…") + let newBranch = try await doCheckoutNewBranch() + print("🪾🆕 Checked out new branch \(newBranch) ☑️.") + } + func doCheckoutNewBranch() async throws -> String { + let newBranch = "bump/libsecp256k1_to_\(newVersion.tag)" + try await runCommand( + "git", + arguments: ["checkout", "-b", newBranch], + workingDirectory: projectRoot + ) + return newBranch + } + + func commitChanges(newVersion: Version) async throws { + print("💾 Committing changes…") + try await doCommitChanges(newVersion: newVersion) + print("💾 Commited changes ☑️.") + } + + func doCommitChanges(newVersion: Version) async throws { let commitMessage = "Update libsecp256k1 dependency to \(newVersion) [all unit tests passed]" - print("💾 Committing changes…") try await runCommand( "git", arguments: ["commit", "-m", commitMessage], - dryRun: dryRun, workingDirectory: projectRoot ) + } + func push(branch: String) async throws { print("🛜🪾 Pushing branch to origin…") + try await doPush(branch: newBranch) + print("🛜🪾 Pushed branch to origin ☑️.") + } + + func doPush(branch: String) async throws { try await runCommand( "git", - arguments: ["push", "--set-upstream", "origin", branchName], + arguments: ["push", "--set-upstream", "origin", branch], dryRun: dryRun, workingDirectory: projectRoot ) } + + func stageSubmoduleChangesIfLive() async throws { + if dryRun { + print("➕🌵 Skipping git add of submodule changes since dry run.") + } else { + try await stageSubmoduleChanges() + } + } + + func stageSubmoduleChanges() async throws { + print("➕📦 Staging submodule changes…") + try await doStageSubmoduleChanges() + print("➕📦 Staged submodule changes ☑️.") + } + + func doStageSubmoduleChanges() async throws { + try await runCommand( + "git", + arguments: ["add", dependencyPath], + workingDirectory: projectRoot + ) + } } // MARK: - CLI @@ -264,7 +328,8 @@ private struct CLI { --root Override project root (defaults to repository root). --dry-run Print actions without mutating the working copy. -h, --help Show this help. - """) + """ + ) exit(0) default: throw ToolError("Unrecognized argument: \(arg)") @@ -347,6 +412,18 @@ extension Program { private func updateReadme( oldVersion: Version, newVersion: Version + ) throws { + print("📝 Updating README.md…") + try doUpdateReadme( + oldVersion: oldVersion, + newVersion: newVersion + ) + print("📝 Updated README.md ☑️.") + } + + private func doUpdateReadme( + oldVersion: Version, + newVersion: Version ) throws { let readmeURL = URL(fileURLWithPath: projectRoot.appending("README.md").description) let content = try String(contentsOf: readmeURL) From a92cfbf03b44f60d022b56810d2cc36f988354a3 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:46:16 +0100 Subject: [PATCH 11/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 6b509ce..186684d 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -90,7 +90,7 @@ extension Program { } try await stageReadme() - let newBranch = try await checkoutNewBranch() + let newBranch = try await checkoutNewBranch(newVersion: newVersion) try await commitChanges(newVersion: newVersion) try await push(branch: newBranch) } @@ -156,9 +156,9 @@ extension Program { } func getCurrentVersion() async throws -> Version { - print("🏷️ Getting current libsecp256k1 version: \(oldVersion)") + print("🏷️ Getting current libsecp256k1 version…") let oldVersion = try await doGetCurrentVersion() - print("🏷️ Got current libsecp256k1 version: \(oldVersion)") + print("🏷️ Got current libsecp256k1 version: \(oldVersion) ☑️.") return oldVersion } @@ -192,9 +192,9 @@ extension Program { } func checkout(tag: String) async throws -> Version { - print("🏷️🔀 Checking out \(latestTag)…") - let latestVersion = try await doCheckout(tag: latestTag) - print("🏷️🔀 Checked out \(latestTag) ☑️.") + print("🏷️🔀 Checking out \(tag)…") + let latestVersion = try await doCheckout(tag: tag) + print("🏷️🔀 Checked out \(tag) ☑️.") return latestVersion } @@ -209,13 +209,13 @@ extension Program { arguments: ["rev-list", "-n", "1", tag], workingDirectory: dependencyFullPath ).stdout.trimmed() - print("#️⃣🆕 Commit resolved from tag: \(commit)") + print("#️⃣ 🆕 Commit resolved from tag: \(commit)") return Version(tag: tag, commit: commit) } func stageReadme() async throws { print("➕📄 Staging README change…") - try await stageReadme() + try await doStageReadme() print("➕📄 Staged README change ☑️.") } @@ -227,13 +227,14 @@ extension Program { ) } - func checkoutNewBranch() async throws -> String { + func checkoutNewBranch(newVersion: Version) async throws -> String { print("🪾🆕 Checked out new branch…") - let newBranch = try await doCheckoutNewBranch() + let newBranch = try await doCheckoutNewBranch(newVersion: newVersion) print("🪾🆕 Checked out new branch \(newBranch) ☑️.") + return newBranch } - func doCheckoutNewBranch() async throws -> String { + func doCheckoutNewBranch(newVersion: Version) async throws -> String { let newBranch = "bump/libsecp256k1_to_\(newVersion.tag)" try await runCommand( "git", @@ -261,7 +262,7 @@ extension Program { func push(branch: String) async throws { print("🛜🪾 Pushing branch to origin…") - try await doPush(branch: newBranch) + try await doPush(branch: branch) print("🛜🪾 Pushed branch to origin ☑️.") } From 5ad517028b6012ad70822717fcb9ac5bd181f6d5 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:47:29 +0100 Subject: [PATCH 12/18] WIP --- .../Sources/UpdateLibsecp/main.swift | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 186684d..580561b 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -102,28 +102,18 @@ extension Program { if let originalError { print("Cleaning up due to error: \(originalError)") } - // Reset submodule change if dry run - if dryRun { + if !dryRun { + // Switch back to working branch do { try await runCommand( "git", - arguments: ["submodule", "update", "--", dependencyPath], + arguments: ["switch", currentBranch], workingDirectory: dependencyFullPath ) } catch { - print("❌ Error while resetting submodule changes: \(error)") + print("❌ Error while switching back to branch '\(currentBranch)': \(error)") } } - // Switch back to working branch - do { - try await runCommand( - "git", - arguments: ["switch", currentBranch], - workingDirectory: dependencyFullPath - ) - } catch { - print("❌ Error while switching back to branch '\(currentBranch)': \(error)") - } } } From 2b9aefc4b574cb35e597729c4f4d35677e45e87c Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:49:06 +0100 Subject: [PATCH 13/18] WIP --- .../update-libsecp/Sources/UpdateLibsecp/main.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 580561b..ffc1f1f 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -102,7 +102,18 @@ extension Program { if let originalError { print("Cleaning up due to error: \(originalError)") } - if !dryRun { + if dryRun { + // Reset submodule change if dry run + do { + try await runCommand( + "git", + arguments: ["submodule", "update", "--", dependencyPath], + workingDirectory: projectRoot + ) + } catch { + print("❌ Error while resetting submodule changes: \(error)") + } + } else { // Switch back to working branch do { try await runCommand( From 76b1826180f0da72a0a9083a6800c610a4ac5000 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:49:49 +0100 Subject: [PATCH 14/18] WIP --- scripts/update-libsecp/Sources/UpdateLibsecp/main.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index ffc1f1f..cdb9a59 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -30,10 +30,7 @@ extension Program { let dryRun = cli.dryRun if dryRun { print("🌵🏃‍♂️ Running in dry-run mode — we won't perform any changes.") - } else { - fatalError("only dry run supported for now") } - let dependencyPath = try readDependencyPath(in: projectRoot) print("📦 Found libsecp256k1 submodule at \(dependencyPath)") return Self(dryRun: dryRun, projectRoot: projectRoot, dependencyPath: dependencyPath) From 6ce9a148ecf9dbbfb691d4cad96c3637eb1e0278 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:54:46 +0100 Subject: [PATCH 15/18] fix path error --- scripts/update-libsecp/Sources/UpdateLibsecp/main.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index cdb9a59..5771e9e 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -116,7 +116,7 @@ extension Program { try await runCommand( "git", arguments: ["switch", currentBranch], - workingDirectory: dependencyFullPath + workingDirectory: projectRoot ) } catch { print("❌ Error while switching back to branch '\(currentBranch)': \(error)") @@ -441,7 +441,8 @@ extension Program { let matches = regex.matches(in: content, options: [], range: range) guard let match = matches.first else { - throw ToolError("Could not find README version line to replace, searched for line:\n\(oldLinePattern)") + throw ToolError( + "Could not find README version line to replace, searched for line:\n\(oldLinePattern)") } if dryRun { @@ -472,7 +473,8 @@ private func firstLineOf( workingDirectory: workingDirectory ) guard let firstLine = stdout.split(separator: "\n").first else { - throw ToolError("No first line returned from command. Output was: '\(stdout)', stderr: '\(stderr)'") + throw ToolError( + "No first line returned from command. Output was: '\(stdout)', stderr: '\(stderr)'") } return String(firstLine) } From ebd2173a6e1663f37162b2f093eb92d19c629718 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 15:59:07 +0100 Subject: [PATCH 16/18] always revert submodule change --- .../Sources/UpdateLibsecp/main.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index 5771e9e..d8b6a00 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -99,18 +99,7 @@ extension Program { if let originalError { print("Cleaning up due to error: \(originalError)") } - if dryRun { - // Reset submodule change if dry run - do { - try await runCommand( - "git", - arguments: ["submodule", "update", "--", dependencyPath], - workingDirectory: projectRoot - ) - } catch { - print("❌ Error while resetting submodule changes: \(error)") - } - } else { + if !dryRun { // Switch back to working branch do { try await runCommand( @@ -122,6 +111,17 @@ extension Program { print("❌ Error while switching back to branch '\(currentBranch)': \(error)") } } + + // Reset submodule change if dry run + do { + try await runCommand( + "git", + arguments: ["submodule", "update", "--", dependencyPath], + workingDirectory: projectRoot + ) + } catch { + print("❌ Error while resetting submodule changes: \(error)") + } } } From 7ca2bb9e62dfea93a13f5e1655976f88907bf030 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 16:02:24 +0100 Subject: [PATCH 17/18] document bumping of submodule --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b4e60da..c3a5c8e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ For each private key there exists two different `signature:for:options` (one tak The `option` is a `K1.ECDSA.SigningOptions` struct, which by default specifies [`RFC6979`][rfc6979] deterministic signing, as per Bitcoin standard, however, you can change to use secure random nonce instead. -### NonRecoverable +### NonRecoverable #### Sign @@ -89,7 +89,7 @@ let hashedMessage: Data = // from somewhere let signature = try alice.signature(for: hashedMessage) ``` -##### Digest +##### Digest ```swift let message: Data = // from somewhere @@ -249,12 +249,23 @@ assert(ab.count == 65) // pass # Development -Stand in root and run to setup submodules +## Setup submodule +Stand in root and run to setup submodule ```sh make submodules ``` +## Update submodule +```sh +just bump-dep +``` + +Or to use dry run: +```sh +just bump-dep true +``` + ## `gyb` Some of the files in this project are autogenerated (metaprogramming) using the Swift Utils tools called [gyb](https://github.com/apple/swift/blob/main/utils/gyb.py) (_"generate your boilerplate"_). `gyb` is included in [`./scripts/gyb`](scripts/gyb). From f2b8b3be38576068edccc650bcf28a9ba7e4f8dc Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Wed, 28 Jan 2026 16:13:34 +0100 Subject: [PATCH 18/18] review fixes --- UPDATE_DEPENDENCY.md | 116 ------------------ .../Sources/UpdateLibsecp/main.swift | 5 +- 2 files changed, 1 insertion(+), 120 deletions(-) delete mode 100644 UPDATE_DEPENDENCY.md diff --git a/UPDATE_DEPENDENCY.md b/UPDATE_DEPENDENCY.md deleted file mode 100644 index 6e34bac..0000000 --- a/UPDATE_DEPENDENCY.md +++ /dev/null @@ -1,116 +0,0 @@ -# Update the libsecp256k1 submodule - -Standing in K1 project root, call it `K1_ROOT`. - -Find path to submodule (libsecp256k1) - -```sh -cat .gitmodules -``` - -Which will print something like: -``` -[submodule "Sources/secp256k1/libsecp256k1"] - path = Sources/secp256k1/libsecp256k1 - url = https://github.com/bitcoin-core/secp256k1.git -``` - -Read the value of `path` and save it into `DEPENDENCY_PATH` (`Sources/secp256k1/libsecp256k1`). - -## Get current (old) tag and commit -Read out the current tag and commit: -```sh -git submodule status | grep $DEPENDENCY_PATH -``` - -which will output something like -``` -1a53f4961f337b4d166c25fce72ef0dc88806618 Sources/secp256k1/libsecp256k1 (v0.7.1) -``` - -save it into `OLD_VERSION`. - -Extract the current (old) commit and tag -```sh -OLD_COMMIT=$(printf '%s\n' "$CURRENT_VERSION" | awk '{print $1}') -OLD_TAG=$(printf '%s\n' "$CURRENT_VERSION" | awk -F'[()]' '{print $2}') -``` - -```sh -cd $DEPENDENCY_PATH -``` - -Git fetch - -```sh -git fetch -``` - -Extract tag with highest semver number (somewhat incorrectly we call it "latest"): -```sh -git tag --list 'v*' --sort=-v:refname | head -n 1 -``` -Call it `LATEST_TAG` - -Checkout latest tag - -```sh -git checkout $LATEST_TAG -``` - -Extract commit from tag -```sh -git rev-list -n 1 $LATEST_TAG -``` -call it `NEW_COMMIT` - -Go back to project root - -```sh -cd $K1_ROOT -``` - -Stage changes -```sh -git add $DEPENDENCY_PATH -``` - -Run tests -```sh -swift test -``` - -If and only if all tests passes, proceed - -## Update documented version in README.md - -in README.md replace old values of - -```text -> Current `libsecp256k1` version is [$OLD_TAG ($OLD_COMMIT)](https://github.com/bitcoin-core/secp256k1/releases/tag/$OLD_TAG) -``` - -with new version: -```text -> Current `libsecp256k1` version is [$LATEST_TAG ($NEW_COMMIT)](https://github.com/bitcoin-core/secp256k1/releases/tag/$LATEST_TAG) -``` - -Stage README.md changes: -```sh -git add README.md -``` - -Checkout new branch -```sh -git checkout -b bump/libsecp256k1_to_$LATEST_TAG -``` - -Commit changes -```sh -git commit -m "Update libsecp256k1 dependency to $LATEST_TAG ($NEW_COMMIT) [all unit tests passed]" -``` - -Push changes -```sh -git push --set-upstream origin $(git_current_branch) -``` diff --git a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift index d8b6a00..c86d472 100644 --- a/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift +++ b/scripts/update-libsecp/Sources/UpdateLibsecp/main.swift @@ -50,7 +50,6 @@ extension Program { do { try await proceed( - branchAtStart: currentBranch, latestVersion: latestVersion, oldVersion: oldVersion ) @@ -69,7 +68,6 @@ extension Program { } func proceed( - branchAtStart: String, latestVersion newVersion: Version, oldVersion: Version ) async throws { @@ -112,7 +110,6 @@ extension Program { } } - // Reset submodule change if dry run do { try await runCommand( "git", @@ -250,7 +247,7 @@ extension Program { func doCommitChanges(newVersion: Version) async throws { let commitMessage = - "Update libsecp256k1 dependency to \(newVersion) [all unit tests passed]" + "Update libsecp256k1 dependency to \(newVersion.tag) (\(newVersion.commit)) [all unit tests passed]" try await runCommand( "git", arguments: ["commit", "-m", commitMessage],