Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 69 additions & 33 deletions Plugins/BenchmarkCommandPlugin/BenchmarkCommandPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// Running the `BenchmarkTool` for each benchmark target.

import PackagePlugin
import Foundation

#if canImport(Darwin)
import Darwin
Expand All @@ -29,6 +30,29 @@ import Glibc
cStrings.forEach { free($0) }
}

/// Build an environment array for posix_spawn that includes extra environment variables.
/// Swift Package Manager plugins run in a sandboxed environment that strips DYLD_* variables,
/// so we need to pass them explicitly via --env arguments.
func withEnvironment(extraEnv: [String], scoped: ([UnsafeMutablePointer<CChar>?]) throws -> Void) rethrows {
var envStrings: [UnsafeMutablePointer<CChar>?] = []
var index = 0
while let envVar = environ[index] {
envStrings.append(strdup(envVar))
index += 1
}
// Add extra environment variables passed via --env
for envVar in extraEnv {
envStrings.append(strdup(envVar))
}
envStrings.append(nil)
defer {
for envVar in envStrings {
free(envVar)
}
}
try scoped(envStrings)
}

func performCommand(context: PluginContext, arguments: [String]) throws {
// Get specific target(s) to run benchmarks for if specified on command line
var argumentExtractor = ArgumentExtractor(arguments)
Expand All @@ -50,6 +74,11 @@ import Glibc
let scale = argumentExtractor.extractFlag(named: "scale")
let helpRequested = argumentExtractor.extractFlag(named: "help")
let otherSwiftFlagsSpecified = argumentExtractor.extractOption(named: "Xswiftc")

// Extract environment variables to pass through (--env NAME=VALUE)
// This is needed because Swift Package Manager plugins run in a sandboxed environment
// that strips DYLD_* and other environment variables
let extraEnvironment = argumentExtractor.extractOption(named: "env")
var outputFormat: OutputFormat = .text
var grouping = "benchmark"
var exportPath = "."
Expand Down Expand Up @@ -219,6 +248,11 @@ import Glibc
args.append(contentsOf: ["--skip", skip])
}

// Pass through environment variables to BenchmarkTool
extraEnvironment.forEach { envVar in
args.append(contentsOf: ["--env", envVar])
}

if pathSpecified.count > 0 {
args.append(contentsOf: ["--path", exportPath])
}
Expand Down Expand Up @@ -474,42 +508,44 @@ import Glibc
return
}

var pid: pid_t = 0
var status = posix_spawn(&pid, benchmarkTool.string, nil, nil, cArgs, environ)

if status == 0 {
if waitpid(pid, &status, 0) != -1 {
// Ok, this sucks, but there is no way to get a C support target for plugins and
// the way the status is extracted portably is with macros - so we just need to
// reimplement the logic here in Swift according to the waitpid man page to
// get some nicer feedback on failure reason.
guard let waitStatus = ExitCode(rawValue: (status & 0xFF00) >> 8) else {
print("One or more benchmarks returned an unexpected return code \(status)")
throw MyError.benchmarkUnexpectedReturnCode
}
switch waitStatus {
case .success:
break
case .baselineNotFound:
throw MyError.baselineNotFound
case .genericFailure:
print("One or more benchmark suites crashed during runtime.")
throw MyError.benchmarkCrashed
case .thresholdRegression:
throw MyError.benchmarkThresholdRegression
case .thresholdImprovement:
throw MyError.benchmarkThresholdImprovement
case .benchmarkJobFailed:
failedBenchmarkCount += 1
case .noPermissions:
throw MyError.noPermissions
try withEnvironment(extraEnv: extraEnvironment) { cEnv in
var pid: pid_t = 0
var status = posix_spawn(&pid, benchmarkTool.string, nil, nil, cArgs, cEnv)

if status == 0 {
if waitpid(pid, &status, 0) != -1 {
// Ok, this sucks, but there is no way to get a C support target for plugins and
// the way the status is extracted portably is with macros - so we just need to
// reimplement the logic here in Swift according to the waitpid man page to
// get some nicer feedback on failure reason.
guard let waitStatus = ExitCode(rawValue: (status & 0xFF00) >> 8) else {
print("One or more benchmarks returned an unexpected return code \(status)")
throw MyError.benchmarkUnexpectedReturnCode
}
switch waitStatus {
case .success:
break
case .baselineNotFound:
throw MyError.baselineNotFound
case .genericFailure:
print("One or more benchmark suites crashed during runtime.")
throw MyError.benchmarkCrashed
case .thresholdRegression:
throw MyError.benchmarkThresholdRegression
case .thresholdImprovement:
throw MyError.benchmarkThresholdImprovement
case .benchmarkJobFailed:
failedBenchmarkCount += 1
case .noPermissions:
throw MyError.noPermissions
}
} else {
print("waitpid() for pid \(pid) returned a non-zero exit code \(status), errno = \(errno)")
exit(errno)
}
} else {
print("waitpid() for pid \(pid) returned a non-zero exit code \(status), errno = \(errno)")
exit(errno)
print("Failed to run BenchmarkTool, posix_spawn() returned [\(status)]")
}
} else {
print("Failed to run BenchmarkTool, posix_spawn() returned [\(status)]")
}
}

Expand Down
88 changes: 58 additions & 30 deletions Plugins/BenchmarkTool/BenchmarkTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ struct BenchmarkTool: AsyncParsableCommand {
@Option(name: .long, help: "Benchmarks matching the regexp filter that should be skipped")
var skip: [String] = []

@Option(name: .long, help: "Environment variables to pass to benchmark subprocesses (NAME=VALUE)")
var env: [String] = []

var inputFD: CInt = 0
var outputFD: CInt = 0

Expand Down Expand Up @@ -353,6 +356,29 @@ struct BenchmarkTool: AsyncParsableCommand {
cStrings.forEach { free($0) }
}

/// Build an environment array for posix_spawn that includes extra environment variables.
/// Swift Package Manager plugins run in a sandboxed environment that strips DYLD_* variables,
/// so we need to pass them explicitly via --env arguments.
func withEnvironment(scoped: ([UnsafeMutablePointer<CChar>?]) throws -> Void) rethrows {
var envStrings: [UnsafeMutablePointer<CChar>?] = []
var index = 0
while let envVar = environ[index] {
envStrings.append(strdup(envVar))
index += 1
}
// Add extra environment variables passed via --env
for envVar in env {
envStrings.append(strdup(envVar))
}
envStrings.append(nil)
defer {
for envVar in envStrings {
free(envVar)
}
}
try scoped(envStrings)
}

enum RunCommandError: Error {
case WaitPIDError
case POSIXSpawnError(Int32)
Expand Down Expand Up @@ -390,40 +416,42 @@ struct BenchmarkTool: AsyncParsableCommand {
outputFD = toChild.writeEnd.rawValue

try withCStrings(args) { cArgs in
var status = posix_spawn(&pid, path.string, nil, nil, cArgs, environ)

// Close child ends of the pipes
try toChild.readEnd.close()
try fromChild.writeEnd.close()

do {
switch benchmarkCommand {
case .`init`:
fatalError("Should never come here")
case .query:
try queryBenchmarks(benchmarkPath) // Get all available benchmarks first
case .list:
try listBenchmarks()
case .baseline, .thresholds, .run:
guard let benchmark else {
fatalError("No benchmark specified for update/export/run/compare operation")
try withEnvironment { cEnv in
var status = posix_spawn(&pid, path.string, nil, nil, cArgs, cEnv)

// Close child ends of the pipes
try toChild.readEnd.close()
try fromChild.writeEnd.close()

do {
switch benchmarkCommand {
case .`init`:
fatalError("Should never come here")
case .query:
try queryBenchmarks(benchmarkPath) // Get all available benchmarks first
case .list:
try listBenchmarks()
case .baseline, .thresholds, .run:
guard let benchmark else {
fatalError("No benchmark specified for update/export/run/compare operation")
}
benchmarkResults = try runBenchmark(target: path.lastComponent!.description, benchmark: benchmark)
}
benchmarkResults = try runBenchmark(target: path.lastComponent!.description, benchmark: benchmark)
}

try write(.end)
} catch {
print("Process failed: \(String(reflecting: error))")
}
try write(.end)
} catch {
print("Process failed: \(String(reflecting: error))")
}

guard status == 0 else {
throw RunCommandError.POSIXSpawnError(status)
}
guard waitpid(pid, &status, 0) != -1 else {
print("waitpiderror")
throw RunCommandError.WaitPIDError
guard status == 0 else {
throw RunCommandError.POSIXSpawnError(status)
}
guard waitpid(pid, &status, 0) != -1 else {
print("waitpiderror")
throw RunCommandError.WaitPIDError
}
completion?(status)
}
completion?(status)
}

return benchmarkResults
Expand Down
Loading
Loading