From c3c31067a47b15b085d165f4cf7ef8aa8802d6e8 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Fri, 14 Nov 2025 10:32:28 -0800 Subject: [PATCH 1/4] [CI] GitHub Actions support for PR testing and automerger (#526) * [CI] GitHub Actions support for PR testing and automerger * Fix python lint failures * Drop License header check and unacceptable lang from GitHub Actions * Don't run linux test in parallel * Update the linux actions to build only for now. * Split up the line to avoid length issue with python linter * Only build for Windows and disable the static sdk for now --- .github/workflows/automerge.yml | 23 ++++++++++++++++++ .github/workflows/pull_request.yml | 39 ++++++++++++++++++++++++++++++ Utilities/build-script-helper.py | 38 +++++++++++++++++++++++------ 3 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/automerge.yml create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 00000000..699967f7 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,23 @@ +name: Create PR to merge release into main branch +# In the first period after branching the release branch, +# we typically want to include many changes from `main` in the release branch. +# This workflow automatically creates a PR to merge the release branch into the main. +# Later in the release cycle we should stop this practice to avoid landing risky changes by disabling this workflow. +# To do so, disable the workflow as described in https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/disabling-and-enabling-a-workflow +permissions: + contents: read +on: + schedule: + - cron: '0 9 * * MON' + workflow_dispatch: +jobs: + create_merge_pr: + name: Create PR to merge release into main branch + uses: swiftlang/github-workflows/.github/workflows/create_automerge_pr.yml@main + with: + head_branch: release/6.3 + base_branch: main + permissions: + contents: write + pull-requests: write + if: (github.event_name == 'schedule' && github.repository == 'swiftlang/swift-tools-support-core') || (github.event_name != 'schedule') # Ensure that we don't run this on a schedule in a fork diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..c81ce761 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,39 @@ +name: Pull request + +permissions: + contents: read + +on: + pull_request: + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]' + linux_swift_versions: '["nightly-main", "nightly-6.2"]' + linux_build_command: 'swift build' + windows_swift_versions: '["nightly-main"]' + windows_build_command: 'Invoke-Program swift build' + enable_android_sdk_build: true + android_sdk_build_command: "swift build --build-tests" + android_ndk_versions: '["r27d", "r29"]' + enable_ios_checks: true + enable_macos_checks: true + macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.3\"}, {\"xcode_version\": \"16.4\"}]" + + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Swift.org" + license_header_check_enabled: false + unacceptable_language_check_enabled: false + api_breakage_check_enabled: false + format_check_enabled: false diff --git a/Utilities/build-script-helper.py b/Utilities/build-script-helper.py index f1357871..3bc4f5aa 100755 --- a/Utilities/build-script-helper.py +++ b/Utilities/build-script-helper.py @@ -15,25 +15,24 @@ from __future__ import print_function import argparse -import json +import errno import os import platform -import re -import shutil -import subprocess import subprocess import sys -import errno + def note(message): print("--- %s: note: %s" % (os.path.basename(sys.argv[0]), message)) sys.stdout.flush() + def error(message): print("--- %s: error: %s" % (os.path.basename(sys.argv[0]), message)) sys.stdout.flush() raise SystemExit(1) + def mkdir_p(path): """Create the given directory, if it does not exist.""" try: @@ -43,6 +42,7 @@ def mkdir_p(path): if e.errno != errno.EEXIST: raise + def call(cmd, cwd=None, verbose=False): """Calls a subprocess.""" if verbose: @@ -54,17 +54,22 @@ def call(cmd, cwd=None, verbose=False): print(' '.join(cmd)) error(str(e)) + def call_output(cmd, cwd=None, stderr=False, verbose=False): """Calls a subprocess for its return data.""" if verbose: print(' '.join(cmd)) try: - return subprocess.check_output(cmd, cwd=cwd, stderr=stderr, universal_newlines=True).strip() + return subprocess.check_output(cmd, + cwd=cwd, + stderr=stderr, + universal_newlines=True).strip() except Exception as e: if not verbose: print(' '.join(cmd)) error(str(e)) + def main(): parser = argparse.ArgumentParser(description=""" This script will build a TSC using CMake. @@ -84,6 +89,7 @@ def main(): # Argument parsing # ----------------------------------------------------------- + def add_global_args(parser): """Configures the parser with the arguments necessary for all actions.""" parser.add_argument( @@ -100,6 +106,7 @@ def add_global_args(parser): action="store_true", help="whether to always reconfigure cmake") + def add_build_args(parser): """Configures the parser with the arguments necessary for build-related actions.""" add_global_args(parser) @@ -116,16 +123,22 @@ def add_build_args(parser): metavar='PATH', help='path to the ninja binary to use for building with CMake') + def parse_global_args(args): """Parses and cleans arguments necessary for all actions.""" args.build_dir = os.path.abspath(args.build_dir) args.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if platform.system() == 'Darwin': - args.sysroot = call_output(["xcrun", "--sdk", "macosx", "--show-sdk-path"], verbose=args.verbose) + args.sysroot = call_output(["xcrun", + "--sdk", + "macosx", + "--show-sdk-path"], + verbose=args.verbose) else: args.sysroot = None + def parse_build_args(args): """Parses and cleans arguments necessary for build-related actions.""" parse_global_args(args) @@ -134,6 +147,7 @@ def parse_build_args(args): args.cmake_path = get_cmake_path(args) args.ninja_path = get_ninja_path(args) + def get_swiftc_path(args): """Returns the path to the Swift compiler.""" if args.swiftc_path: @@ -154,6 +168,7 @@ def get_swiftc_path(args): return swiftc_path + def get_cmake_path(args): """Returns the path to CMake.""" if args.cmake_path: @@ -167,6 +182,7 @@ def get_cmake_path(args): else: return call_output(["which", "cmake"], verbose=args.verbose) + def get_ninja_path(args): """Returns the path to Ninja.""" if args.ninja_path: @@ -184,6 +200,7 @@ def get_ninja_path(args): # Actions # ----------------------------------------------------------- + def build(args): parse_build_args(args) build_tsc(args) @@ -192,10 +209,13 @@ def build(args): # Build functions # ----------------------------------------------------------- + def build_with_cmake(args, cmake_args, source_path, build_dir): """Runs CMake if needed, then builds with Ninja.""" cache_path = os.path.join(build_dir, "CMakeCache.txt") - if args.reconfigure or not os.path.isfile(cache_path) or not args.swiftc_path in open(cache_path).read(): + if args.reconfigure or \ + not os.path.isfile(cache_path) \ + or args.swiftc_path not in open(cache_path).read(): swift_flags = "" if args.sysroot: swift_flags = "-sdk %s" % args.sysroot @@ -222,6 +242,7 @@ def build_with_cmake(args, cmake_args, source_path, build_dir): call(ninja_cmd, cwd=build_dir, verbose=args.verbose) + def build_tsc(args): cmake_flags = [] if platform.system() == 'Darwin': @@ -230,5 +251,6 @@ def build_tsc(args): build_with_cmake(args, cmake_flags, args.project_root, args.build_dir) + if __name__ == '__main__': main() From ce3979d5303244719b5877b2493ae6e02ad23588 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Mon, 17 Nov 2025 17:49:18 -0800 Subject: [PATCH 2/4] Update automerge workflow to merge main into release (#528) * Update automerge workflow to merge main into release * Update .github/workflows/automerge.yml Co-authored-by: Owen Voorhees --------- Co-authored-by: Owen Voorhees --- .github/workflows/automerge.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 699967f7..d24b8173 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,7 +1,7 @@ -name: Create PR to merge release into main branch +name: Create PR to merge main into release branch # In the first period after branching the release branch, # we typically want to include many changes from `main` in the release branch. -# This workflow automatically creates a PR to merge the release branch into the main. +# This workflow automatically creates a PR to merge the main into the release branch. # Later in the release cycle we should stop this practice to avoid landing risky changes by disabling this workflow. # To do so, disable the workflow as described in https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/disabling-and-enabling-a-workflow permissions: @@ -12,11 +12,11 @@ on: workflow_dispatch: jobs: create_merge_pr: - name: Create PR to merge release into main branch + name: Create PR to merge main into release branch uses: swiftlang/github-workflows/.github/workflows/create_automerge_pr.yml@main with: - head_branch: release/6.3 - base_branch: main + head_branch: main + base_branch: release/6.3 permissions: contents: write pull-requests: write From b157c62a8e2d4cc01089c222e8cd5c5a89be5728 Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Wed, 19 Nov 2025 11:03:58 -0800 Subject: [PATCH 3/4] Add static linux SDK actions coverage (#527) --- .github/workflows/pull_request.yml | 2 ++ Package.swift | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c81ce761..fbbee69d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,6 +19,8 @@ jobs: linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]' linux_swift_versions: '["nightly-main", "nightly-6.2"]' linux_build_command: 'swift build' + enable_linux_static_sdk_build: true + linux_static_sdk_build_command: SWIFTTOOLSSUPPORTCORE_STATIC_LINK=1 swift build windows_swift_versions: '["nightly-main"]' windows_build_command: 'Invoke-Program swift build' enable_android_sdk_build: true diff --git a/Package.swift b/Package.swift index 16a036b3..18ece6db 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,8 @@ if let deploymentTarget = ProcessInfo.processInfo.environment["SWIFTTSC_IOS_DEPL iOSPlatform = .iOS(.v13) } +let isStaticBuild = ProcessInfo.processInfo.environment["SWIFTTOOLSSUPPORTCORE_STATIC_LINK"] != nil + let CMakeFiles = ["CMakeLists.txt"] let package = Package( @@ -115,3 +117,12 @@ let package = Package( exclude: ["pkgconfigInputs", "Inputs"]), ] ) + +if isStaticBuild { + package.targets = package.targets.filter { target in + target.type != .test && !target.name.hasSuffix("TestSupport") + } + package.products = package.products.filter { product in + !product.name.hasSuffix("TestSupport") && product.name != "SwiftToolsSupport" + } +} From 84d81b7afa0e12e83bf0504fefa2bcff7367b2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fo=C5=99t?= Date: Tue, 2 Dec 2025 22:18:12 +0100 Subject: [PATCH 4/4] Fix LocalFileSystem.move() to not follow symlinks when checking source existence (#530) The move() method was using exists(sourcePath) which follows symlinks by default. This caused failures when moving a symlink whose target had already been moved, because it would try to follow the symlink to validate the target exists. Now uses exists(sourcePath, followSymlink: false) to check if the symlink itself exists, allowing symlinks to be moved even if their targets have been relocated. This fixes an issue where extracting package archives with symlinks could fail if files were moved in alphabetical order and a target file was moved before its symlink. Added test testMoveSymlinkWithMovedTarget() to verify this behavior. --- Sources/TSCBasic/FileSystem.swift | 2 +- Tests/TSCBasicTests/FileSystemTests.swift | 41 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Sources/TSCBasic/FileSystem.swift b/Sources/TSCBasic/FileSystem.swift index 1ee457bc..69e39e89 100644 --- a/Sources/TSCBasic/FileSystem.swift +++ b/Sources/TSCBasic/FileSystem.swift @@ -735,7 +735,7 @@ private struct LocalFileSystem: FileSystem { } func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) } + guard exists(sourcePath, followSymlink: false) else { throw FileSystemError(.noEntry, sourcePath) } guard !exists(destinationPath) else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) } do { diff --git a/Tests/TSCBasicTests/FileSystemTests.swift b/Tests/TSCBasicTests/FileSystemTests.swift index e31b27cc..27e5bdc4 100644 --- a/Tests/TSCBasicTests/FileSystemTests.swift +++ b/Tests/TSCBasicTests/FileSystemTests.swift @@ -511,6 +511,47 @@ class FileSystemTests: XCTestCase { } } + func testMoveSymlinkWithMovedTarget() throws { + let fs = TSCBasic.localFileSystem + + try testWithTemporaryDirectory { tmpdir in + let sourceDir = tmpdir.appending(component: "source") + let destDir = tmpdir.appending(component: "dest") + + try fs.createDirectory(sourceDir) + try fs.createDirectory(destDir) + + // Create a regular file that will be the symlink target + let targetFile = sourceDir.appending(component: "AFile.swift") + try fs.writeFileContents(targetFile, bytes: "// Target file content\n") + + // Create a relative symlink pointing to the target file + let symlink = sourceDir.appending(component: "ZLinkToFile.swift") + try fs.createSymbolicLink(symlink, pointingAt: targetFile, relative: true) + + XCTAssertTrue(fs.isSymlink(symlink)) + XCTAssertTrue(fs.exists(symlink)) + + // Move the target file first + let movedTarget = destDir.appending(component: "AFile.swift") + try fs.move(from: targetFile, to: movedTarget) + + // Now try to move the symlink - this should succeed even though its target has moved + // The symlink's target will be broken after the move, but the symlink itself should be moveable + let movedSymlink = destDir.appending(component: "ZLinkToFile.swift") + XCTAssertNoThrow(try fs.move(from: symlink, to: movedSymlink)) + + XCTAssertFalse(fs.exists(symlink, followSymlink: false)) + XCTAssertTrue(fs.exists(movedSymlink, followSymlink: false)) + XCTAssertTrue(fs.isSymlink(movedSymlink)) + + XCTAssertTrue(fs.exists(movedSymlink, followSymlink: true)) + let symlinkContent = try fs.readFileContents(movedSymlink) + let targetContent = try fs.readFileContents(movedTarget) + XCTAssertEqual(symlinkContent, targetContent) + } + } + // MARK: InMemoryFileSystem Tests func testInMemoryBasics() throws {