diff --git a/Jenkinsfile b/Jenkinsfile index 6ca30e1368849e8644022dc375cc698a748c2852..a014da2ced8c91715e6145462ee1648d9034db39 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,13 +1,15 @@ #!/usr/bin/env groovy -def ciResults = [:] -def teeResults = [:] +import groovy.transform.Field + +@Field Map ciResults = [:] +@Field Map teeResults = [:] pipeline { agent { docker { image 'yeanwang/x-kernel-builder:v1.5' - args '-v /var/run/docker.sock:/var/run/docker.sock -v /var/jenkins_home/cargo/registry:/usr/local/cargo/registry -v /var/jenkins_home/.rustup/toolchains:/usr/local/rustup/toolchains -v /var/jenkins_home/xkernel-target:/xkernel-target --privileged -u root:root' + args '-v /var/run/docker.sock:/var/run/docker.sock -v /var/jenkins_home/cargo/registry:/usr/local/cargo/registry -v /var/jenkins_home/cargo/config.toml:/usr/local/cargo/config.toml:ro -v /var/jenkins_home/.rustup/toolchains:/usr/local/rustup/toolchains -v /var/jenkins_home/xkernel-target:/xkernel-target --privileged -u root:root' } } @@ -15,6 +17,8 @@ pipeline { skipDefaultCheckout(true) timestamps() parallelsAlwaysFailFast() + timeout(time: 90, unit: 'MINUTES') + buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10')) } environment { @@ -31,204 +35,49 @@ pipeline { RUSTUP_PERMIT_COPY_RENAME = '1' PYTHONUNBUFFERED = '1' TARGET_DIR = '/xkernel-target' + HARNESS_JOBS = '2' } stages { - stage('Prepare Source') { + stage('Source: Checkout & PR Base') { steps { script { env.ROOT_WS = env.WORKSPACE currentBuild.description = "PR#${env.giteePullRequestIid ?: 'manual'}" - prepareSource() - // 先创建 6 个并行检查占位(较早创建 → Gitee 列表靠下);顺序 4 项在后续 start/finish - giteeStartParallelCheckRuns() - giteeStartCheckRun('Prepare Source') - ciResults['Prepare Source'] = [status: 'passed'] - } - } - post { - success { - script { ciResults['Prepare Source'] = [status: 'passed'] } - } - failure { - script { ciResults['Prepare Source'] = [status: 'failed', detail: '源码准备失败(可能是分支分叉需要 rebase)'] } - } - always { - script { ciFinishGiteeStage('Prepare Source', ciResults, '源码准备失败(可能是分支分叉需要 rebase)') } + runCiStage(sourceStageName(), ciFailureDetail(sourceStageName()), false) { + prepareSource() + // 先创建 6 个并行检查占位(较早创建 -> Gitee 列表靠下);顺序 3 项在后续 start/finish + giteeStartParallelCheckRuns() + giteeStartCheckRun(sourceStageName()) + } } } } - stage('Check Environment') { + stage('Setup: Toolchains & Targets') { steps { script { - giteeStartCheckRun('Check Environment') - checkBuildEnvironment() - ciResults['Check Environment'] = [status: 'passed'] - } - } - post { - success { - script { ciResults['Check Environment'] = [status: 'passed'] } - } - failure { - script { ciResults['Check Environment'] = [status: 'failed', detail: 'Rust 工具链组件或 target 安装失败'] } - } - always { - script { ciFinishGiteeStage('Check Environment', ciResults, 'Rust 工具链组件或 target 安装失败') } + runCiStage(setupStageName(), ciFailureDetail(setupStageName())) { + checkBuildEnvironment() + } } } } - stage('Rustfmt') { + stage('Check: Rustfmt') { steps { script { - giteeStartCheckRun('Rustfmt') - runRustfmt() - ciResults['Rustfmt'] = [status: 'passed'] - } - } - post { - success { - script { ciResults['Rustfmt'] = [status: 'passed'] } - } - failure { - script { ciResults['Rustfmt'] = [status: 'failed', detail: 'cargo fmt --check 发现格式问题'] } - } - always { - script { ciFinishGiteeStage('Rustfmt', ciResults, 'cargo fmt --check 发现格式问题') } + runCiStage(rustfmtStageName(), ciFailureDetail(rustfmtStageName())) { + runRustfmt() + } } } } - stage('Prefetch Dependencies') { + stage('Build & Test') { steps { script { - giteeStartCheckRun('Prefetch Dependencies') - prefetchCargoDeps() - ciResults['Prefetch Dependencies'] = [status: 'passed'] - } - } - post { - success { - script { ciResults['Prefetch Dependencies'] = [status: 'passed'] } - } - failure { - script { ciResults['Prefetch Dependencies'] = [status: 'failed', detail: 'cargo fetch 失败'] } - } - always { - script { ciFinishGiteeStage('Prefetch Dependencies', ciResults, 'cargo fetch 失败') } - } - } - } - - stage('Build & Test') { - parallel { - stage('Clippy+Build: aarch64-crosvm-virt') { - steps { - script { - ciParallelStageBegin('Clippy+Build: aarch64-crosvm-virt', ciResults) - giteeEnsureCheckRunStarted('Clippy+Build: aarch64-crosvm-virt') - runClippyAndBuild('aarch64-crosvm-virt') - ciResults['Clippy+Build: aarch64-crosvm-virt'] = [status: 'passed'] - } - } - post { - success { script { ciResults['Clippy+Build: aarch64-crosvm-virt'] = [status: 'passed'] } } - failure { script { ciMarkParallelFailure('Clippy+Build: aarch64-crosvm-virt', ciResults, 'clippy 或 build 失败') } } - aborted { script { ciResults['Clippy+Build: aarch64-crosvm-virt'] = [status: 'skipped', detail: '因其他并行阶段失败而中止(fail-fast)'] } } - always { script { ciFinishParallelStage('Clippy+Build: aarch64-crosvm-virt', ciResults) } } - } - } - stage('Clippy+Runtime: riscv64-qemu-virt') { - steps { - script { - ciParallelStageBegin('Clippy+Runtime: riscv64-qemu-virt', ciResults) - giteeEnsureCheckRunStarted('Clippy+Runtime: riscv64-qemu-virt') - runClippyAndRuntime('riscv64') - ciResults['Clippy+Runtime: riscv64-qemu-virt'] = [status: 'passed'] - } - } - post { - success { script { ciResults['Clippy+Runtime: riscv64-qemu-virt'] = [status: 'passed'] } } - failure { script { ciMarkParallelFailure('Clippy+Runtime: riscv64-qemu-virt', ciResults, '阶段失败,详见下方日志。') } } - aborted { script { ciResults['Clippy+Runtime: riscv64-qemu-virt'] = [status: 'skipped', detail: '因其他并行阶段失败而中止(fail-fast)'] } } - always { script { ciFinishParallelStage('Clippy+Runtime: riscv64-qemu-virt', ciResults) } } - } - } - stage('Clippy+Runtime: x86_64-qemu-virt') { - steps { - script { - ciParallelStageBegin('Clippy+Runtime: x86_64-qemu-virt', ciResults) - giteeEnsureCheckRunStarted('Clippy+Runtime: x86_64-qemu-virt') - runClippyAndRuntime('x86_64') - ciResults['Clippy+Runtime: x86_64-qemu-virt'] = [status: 'passed'] - } - } - post { - success { script { ciResults['Clippy+Runtime: x86_64-qemu-virt'] = [status: 'passed'] } } - failure { script { ciMarkParallelFailure('Clippy+Runtime: x86_64-qemu-virt', ciResults, '阶段失败,详见下方日志。') } } - aborted { script { ciResults['Clippy+Runtime: x86_64-qemu-virt'] = [status: 'skipped', detail: '因其他并行阶段失败而中止(fail-fast)'] } } - always { script { ciFinishParallelStage('Clippy+Runtime: x86_64-qemu-virt', ciResults) } } - } - } - stage('Clippy+Runtime: aarch64-qemu-virt') { - steps { - script { - ciParallelStageBegin('Clippy+Runtime: aarch64-qemu-virt', ciResults) - giteeEnsureCheckRunStarted('Clippy+Runtime: aarch64-qemu-virt') - runClippyAndRuntime('aarch64') - ciResults['Clippy+Runtime: aarch64-qemu-virt'] = [status: 'passed'] - } - } - post { - success { script { ciResults['Clippy+Runtime: aarch64-qemu-virt'] = [status: 'passed'] } } - failure { script { ciMarkParallelFailure('Clippy+Runtime: aarch64-qemu-virt', ciResults, '阶段失败,详见下方日志。') } } - aborted { script { ciResults['Clippy+Runtime: aarch64-qemu-virt'] = [status: 'skipped', detail: '因其他并行阶段失败而中止(fail-fast)'] } } - always { script { ciFinishParallelStage('Clippy+Runtime: aarch64-qemu-virt', ciResults) } } - } - } - stage('TEE: x86_64') { - steps { - script { - ciParallelStageBegin('TEE: x86_64', ciResults) - giteeEnsureCheckRunStarted('TEE: x86_64') - teeResults['x86_64'] = runTeeStorageTest('x86_64') - ciResults['TEE: x86_64'] = [status: 'passed'] - } - } - post { - failure { script { - if (!teeResults.containsKey('x86_64')) { - teeResults['x86_64'] = [arch: 'x86_64', passed: 0, failed: 0, status: 'failed', errorSnippet: '构建或启动阶段失败,请查看 Jenkins 日志'] - } - ciMarkParallelFailure('TEE: x86_64', ciResults, teeResults['x86_64']?.errorSnippet ?: 'TEE 测试失败') - } } - aborted { script { ciResults['TEE: x86_64'] = [status: 'skipped', detail: '因其他并行阶段失败而中止(fail-fast)'] } } - success { script { ciResults['TEE: x86_64'] = [status: 'passed'] } } - always { script { ciFinishParallelStage('TEE: x86_64', ciResults) } } - } - } - stage('TEE: aarch64') { - steps { - script { - ciParallelStageBegin('TEE: aarch64', ciResults) - giteeEnsureCheckRunStarted('TEE: aarch64') - teeResults['aarch64'] = runTeeStorageTest('aarch64') - ciResults['TEE: aarch64'] = [status: 'passed'] - } - } - post { - failure { script { - if (!teeResults.containsKey('aarch64')) { - teeResults['aarch64'] = [arch: 'aarch64', passed: 0, failed: 0, status: 'failed', errorSnippet: '构建或启动阶段失败,请查看 Jenkins 日志'] - } - ciMarkParallelFailure('TEE: aarch64', ciResults, teeResults['aarch64']?.errorSnippet ?: 'TEE 测试失败') - } } - aborted { script { ciResults['TEE: aarch64'] = [status: 'skipped', detail: '因其他并行阶段失败而中止(fail-fast)'] } } - success { script { ciResults['TEE: aarch64'] = [status: 'passed'] } } - always { script { ciFinishParallelStage('TEE: aarch64', ciResults) } } - } + parallel ciParallelBranches() } } } @@ -236,357 +85,303 @@ pipeline { } post { - aborted { - script { - markCiStagesAborted(ciResults) - } - } always { script { - restoreReplayGiteeEnv() - fixWorkspaceOwnership(env.WORKSPACE) - def failedStageLogs = archiveFailedStageLogs(ciResults) - archiveArtifacts artifacts: [ - 'stage-logs/**/*.log', - '**/artifacts/**/*', '**/logs/**/*', '**/unittest-output.log', - '**/tee-test-output.log', - '**/coverage-html/**/*', '**/coverage.info', '**/coverage.xml', '**/coverage.txt' - ].join(','), allowEmptyArchive: true - def coverageSummary = collectCoverageSummary() - deleteOldCiComments() - def built = buildCombinedComment(ciResults, coverageSummary, failedStageLogs) - notifyGiteePullRequest(built.comment) - giteeFinalizeAllCheckRuns(ciResults, failedStageLogs) - giteeRefreshFailedCheckOutputs(ciResults, failedStageLogs) - // Gitee 检查列表按更新时间排序:构建末尾按顺序刷新 4 个顺序 stage,使其排在 6 个并行之上 - giteeReorderSequentialCheckRuns(ciResults, failedStageLogs) - if (currentBuild.currentResult == 'SUCCESS') { - giteeTestPass() - } else { - giteeTestReset() - } - fixWorkspaceOwnership(env.WORKSPACE) + finalizeCiBuild() } cleanWs deleteDirs: true, disableDeferredWipeout: true, notFailBuild: true } } } -def prefetchCargoDeps() { - initStageLog('Prefetch Dependencies') - ws("${env.ROOT_WS}/prefetch") { - def stageWorkspace = pwd() - try { - deleteDir() - restoreSource() - sh '''#!/bin/bash -set -euo pipefail -''' + stageLogTeeLine('Prefetch Dependencies') + ''' -echo "==> Prefetching cargo dependencies for all platforms..." - -declare -A ARCH_TARGET=( - [aarch64]=aarch64-unknown-none-softfloat - [x86_64]=x86_64-unknown-none - [riscv64]=riscv64gc-unknown-none-elf -) -for platform in x86_64-qemu-virt aarch64-qemu-virt aarch64-crosvm-virt; do - arch="${platform%%-*}" - target="${ARCH_TARGET[$arch]}" - cp "platforms/${platform}/defconfig" .config - cargo fetch --manifest-path entry/Cargo.toml --target "$target" || true -done +def sourceStageName() { return 'Source: Checkout & PR Base' } +def setupStageName() { return 'Setup: Toolchains & Targets' } +def rustfmtStageName() { return 'Check: Rustfmt' } -cargo fetch --manifest-path tee_apps/sh/Cargo.toml || true -cargo fetch --manifest-path xtask/crate_rootfs/Cargo.toml || true +def runtimeTestArchitectures() { return ['x86_64', 'aarch64', 'riscv64'] } +def teeTestArchitectures() { return ['x86_64', 'aarch64'] } -echo "==> Dependency prefetch complete" -''' - } finally { - fixWorkspaceOwnership(stageWorkspace) - } +def ciSequentialStages() { + return [ + [name: sourceStageName(), failure: '源码准备失败(可能是分支分叉需要 rebase)'], + [name: setupStageName(), failure: 'Rust 工具链组件或 target 安装失败'], + [name: rustfmtStageName(), failure: 'cargo fmt --check 发现格式问题'], + ] +} + +def ciParallelStages() { + def stages = [[ + name: 'Build Check: aarch64-crosvm-virt', + failure: 'clippy 或 build 失败', + type: 'build', + platform: 'aarch64-crosvm-virt', + ]] + + runtimeTestArchitectures().each { arch -> + stages << [ + name: "Runtime Test: ${arch}-qemu-virt", + failure: 'clippy、单元测试、覆盖率或 runtime 测试失败', + type: 'runtime', + arch: arch, + ] + } + + teeTestArchitectures().each { arch -> + stages << [ + name: "TEE Storage: ${arch}", + failure: 'TEE 测试失败', + type: 'tee', + arch: arch, + ] } + + return stages } -def checkBuildEnvironment() { - initStageLog('Check Environment') - ws("${env.ROOT_WS}/env-check") { - def stageWorkspace = pwd() - try { - deleteDir() - restoreSource() - sh '''#!/bin/bash -set -euo pipefail -''' + stageLogTeeLine('Check Environment') + ''' -echo "==> Checking Rust build environment..." -NIGHTLY_TOOLCHAIN="${AUX_RUST_TOOLCHAIN}" - -retry() { - local attempts="$1" - shift - local i - for i in $(seq 1 "${attempts}"); do - "$@" && return 0 - if [ "${i}" = "${attempts}" ]; then - return 1 - fi - echo "Command failed, retrying (${i}/${attempts}): $*" >&2 - sleep 5 - done -} - -eval "$( -python3 <<'PY' -import shlex -import tomllib - -with open("rust-toolchain.toml", "rb") as f: - toolchain = tomllib.load(f)["toolchain"] - -def array(name, values): - print(f"{name}=(" + " ".join(shlex.quote(v) for v in values) + ")") - -print("XKERNEL_TOOLCHAIN=" + shlex.quote(toolchain["channel"])) -array("XKERNEL_COMPONENTS", toolchain.get("components", [])) -array("XKERNEL_TARGETS", toolchain.get("targets", [])) -PY -)" +def ciStageNames(List stages) { + return stages.collect { it.name } +} -DEFAULT_EXTRA_TARGETS=( - x86_64-unknown-uefi - x86_64-unknown-linux-musl - aarch64-unknown-linux-musl - riscv64gc-unknown-linux-musl -) -NIGHTLY_TARGETS=( - x86_64-unknown-linux-musl - aarch64-unknown-linux-musl - riscv64gc-unknown-linux-musl -) +def ciFailureDetail(String stageName) { + def stage = (ciSequentialStages() + ciParallelStages()).find { it.name == stageName } + return stage?.failure ?: "${stageName} 失败,请查看 Jenkins 日志" +} -dedup_words() { - printf '%s\n' "$@" | awk 'NF && !seen[$0]++' +def archiveArtifactPatterns() { + return [ + 'ci-summary.md', + 'stage-logs/**/*.log', + '**/artifacts/**/*', + '**/logs/**/*', + '**/unittest-output.log', + '**/tee-test-output.log', + '**/coverage-html/**/*', + '**/coverage.info', + '**/coverage.xml', + '**/coverage.txt', + ] } -mapfile -t DEFAULT_TARGETS < <(dedup_words "${XKERNEL_TARGETS[@]}" "${DEFAULT_EXTRA_TARGETS[@]}") +def finalizeCiBuild() { + restoreReplayGiteeEnv() + fixWorkspaceOwnership(env.WORKSPACE) + + def failedStageLogs = archiveFailedStageLogs(ciResults) + archiveArtifacts artifacts: archiveArtifactPatterns().join(','), allowEmptyArchive: true -default_install_args=("${XKERNEL_TOOLCHAIN}" --profile minimal --no-self-update) -for component in "${XKERNEL_COMPONENTS[@]}"; do - default_install_args+=(--component "${component}") -done -for target in "${DEFAULT_TARGETS[@]}"; do - default_install_args+=(--target "${target}") -done + deleteOldCiComments() + def coverageSummary = collectCoverageSummary() + def built = buildCombinedComment(ciResults, coverageSummary, failedStageLogs) + writeFile file: 'ci-summary.md', text: built.comment.replaceFirst(/^\n/, '') + currentBuild.description = buildShortBuildDescription(ciResults) + notifyGiteePullRequest(built.comment) -nightly_install_args=("${NIGHTLY_TOOLCHAIN}" --profile minimal --component rustfmt --no-self-update) -for target in "${NIGHTLY_TARGETS[@]}"; do - nightly_install_args+=(--target "${target}") -done + giteeFinalizeAllCheckRuns(ciResults, failedStageLogs) + giteeRefreshFailedCheckOutputs(ciResults, failedStageLogs) + giteeReorderSequentialCheckRuns(ciResults, failedStageLogs) + + if (currentBuild.currentResult == 'SUCCESS') { + giteeTestPass() + } else { + giteeTestReset() + } -echo "==> Installing x-kernel toolchain: ${XKERNEL_TOOLCHAIN}" -retry 3 rustup toolchain install "${default_install_args[@]}" + fixWorkspaceOwnership(env.WORKSPACE) +} -echo "==> Installing auxiliary nightly toolchain: ${NIGHTLY_TOOLCHAIN}" -retry 3 rustup toolchain install "${nightly_install_args[@]}" +def buildShortBuildDescription(Map results) { + def pr = env.giteePullRequestIid?.trim() ? "PR#${env.giteePullRequestIid}" : 'manual' + def sha = resolveHeadSha()?.take(8) ?: env.GIT_COMMIT?.take(8) ?: '' + def failedStages = ciStageOrder().findAll { results[it]?.status == 'failed' } + if (currentBuild.currentResult == 'SUCCESS' && failedStages.isEmpty()) { + return "${pr} ✅ ${sha}".trim() + } + if (!failedStages.isEmpty()) { + def failed = failedStages.take(2).join(', ') + def suffix = failedStages.size() > 2 ? " +${failedStages.size() - 2}" : '' + return "${pr} ❌ ${failed}${suffix} ${sha}".trim() + } + return "${pr} ❌ ${currentBuild.currentResult} ${sha}".trim() +} -echo "==> Active default toolchain" -cargo --version -rustc --version -rustup show active-toolchain +def runCiStage(String stageName, String failedDetail, Closure body) { + runCiStage(stageName, failedDetail, true, body) +} -echo "==> Installed default targets" -rustup target list --installed +def runCiStage(String stageName, String failedDetail, boolean startCheckRun, Closure body) { + if (startCheckRun) { + giteeStartCheckRun(stageName) + } -echo "==> Installed nightly targets" -rustup +"${NIGHTLY_TOOLCHAIN}" target list --installed -''' - } finally { - fixWorkspaceOwnership(stageWorkspace) + try { + body.call() + ciResults[stageName] = [status: 'passed'] + } catch (e) { + ciResults[stageName] = [status: 'failed', detail: buildFailureDetail(stageName, failedDetail, e)] + throw e + } finally { + ciFinishGiteeStage(stageName, ciResults, failedDetail) + } +} + +def buildFailureDetail(String stageName, String defaultDetail, Throwable error) { + def details = [] + if (defaultDetail?.trim()) { + details << defaultDetail.trim() + } + + def message = error?.message?.trim() + if (message && !details.any { message.contains(it) }) { + details << "Jenkins 异常: ${message}" + } + + return details ? details.join('\n') : "${stageName} 失败,请查看 Jenkins 日志" +} + +def ciParallelBranches() { + def branches = [:] + + ciParallelStages().each { stageSpec -> + def spec = stageSpec + branches[spec.name] = { + runParallelCiStage(spec.name, spec.failure) { + runCiWorkload(spec) + } } } + + branches.failFast = true + return branches } -def runRustfmt() { - initStageLog('Rustfmt') - ws("${env.ROOT_WS}/rustfmt") { +def runCiWorkload(Map spec) { + switch (spec.type) { + case 'build': + runClippyAndBuild(spec.platform) + break + case 'runtime': + runClippyAndRuntime(spec.arch) + break + case 'tee': + teeResults[spec.arch] = runTeeStorageTest(spec.arch) + break + default: + error("Unsupported CI stage type: ${spec.type}") + } +} + +def runParallelCiStage(String stageName, String failedDetail, Closure body) { + stage(stageName) { + runCiStage(stageName, failedDetail, false) { + giteeEnsureCheckRunStarted(stageName) + body.call() + } + } +} + +def withCleanSourceWorkspace(String relativePath, Closure body) { + ws("${env.ROOT_WS}/${relativePath}") { def stageWorkspace = pwd() try { deleteDir() restoreSource() - sh """#!/bin/bash -set -euo pipefail -${stageLogTeeLine('Rustfmt')} -cargo +"${AUX_RUST_TOOLCHAIN}" fmt --all --check -""" + body.call(stageWorkspace) } finally { fixWorkspaceOwnership(stageWorkspace) } } } +def checkBuildEnvironment() { + initStageLog(setupStageName()) + withCleanSourceWorkspace('env-check') { + sh label: 'Install Rust toolchains and targets', script: """#!/bin/bash +set -euo pipefail +${stageLogTeeLine(setupStageName())} +scripts/ci/check_build_environment.sh +""" + } +} + +def runRustfmt() { + initStageLog(rustfmtStageName()) + withCleanSourceWorkspace('rustfmt') { + sh label: 'Check Rust formatting', script: """#!/bin/bash +set -euo pipefail +${stageLogTeeLine(rustfmtStageName())} +cargo +"${AUX_RUST_TOOLCHAIN}" fmt --all --check +""" + } +} + def runClippyAndBuild(String platform) { - def stageName = "Clippy+Build: ${platform}" + def stageName = "Build Check: ${platform}" initStageLog(stageName) - ws("${env.ROOT_WS}/clippy-build-${platform}") { - def stageWorkspace = pwd() - def buildTargetDir = "/xkernel-target/build-${platform}" - try { - deleteDir() - restoreSource() - - withEnv(["TARGET_DIR=${buildTargetDir}"]) { - sh """#!/bin/bash + def buildTargetDir = "/xkernel-target/build-${platform}" + withCleanSourceWorkspace("clippy-build-${platform}") { + withEnv(["TARGET_DIR=${buildTargetDir}"]) { + sh label: "Clippy and build ${platform}", script: """#!/bin/bash set -euo pipefail ${stageLogTeeLine(stageName)} cp platforms/${platform}/defconfig .config make clippy stdbuf -oL -eL make build """ - } - } finally { - fixWorkspaceOwnership(stageWorkspace) } } } def runClippyAndRuntime(String arch) { def platform = "${arch}-qemu-virt" - def stageName = "Clippy+Runtime: ${platform}" + def stageName = "Runtime Test: ${platform}" def stageLog = stageLogFile(stageName) initStageLog(stageName) def runtimeTargetDir = targetDirForArch(arch) - ws("${env.ROOT_WS}/${arch}") { - def stageWorkspace = pwd() - try { - deleteDir() - restoreSource() - withEnv(["TARGET_DIR=${runtimeTargetDir}", "STAGE_LOG=${stageLog}"]) { - sh """#!/bin/bash + withCleanSourceWorkspace(arch) { + withEnv(["TARGET_DIR=${runtimeTargetDir}", "STAGE_LOG=${stageLog}"]) { + sh label: "Clippy ${platform}", script: """#!/bin/bash set -euo pipefail ${stageLogTeeLine(stageName)} cp platforms/${platform}/defconfig .config make clippy """ - runUnitTests(arch) - generateCoverageHtml(arch) - copyCoverageToWorkspace(arch) + runUnitTests(arch) + generateCoverageHtml(arch) + copyCoverageToWorkspace(arch) - sh """#!/bin/bash + sh label: "Build ${platform}", script: """#!/bin/bash set -euo pipefail ${stageLogTeeLine(stageName)} cp platforms/${platform}/defconfig .config stdbuf -oL -eL make build """ - dir('test-harness') { - git branch: "${env.TEST_HARNESS_BRANCH}", - url: "${env.TEST_HARNESS_REPO}" - markSafeDirectory() - - def hostfwdPort - def vsockCid - switch (arch) { - case 'x86_64': - hostfwdPort = '5556' - vsockCid = '101' - break - case 'aarch64': - hostfwdPort = '5557' - vsockCid = '102' - break - case 'riscv64': - hostfwdPort = '5560' - vsockCid = '103' - break - default: - error("Unsupported runtime test architecture: ${arch}") - } - withEnv(["XKERNEL_REMOTE=${pwd()}/..", "ARCH=${arch}", - "STARRY_SKIP_BUILD=1", - "ROOTFS_CACHE_DIR=/xkernel-target/rootfs-cache", - "GUEST_CASES_TARGET_DIR=${runtimeTargetDir}/guest-cases-${arch}"]) { - sh """#!/bin/bash + dir('test-harness') { + git branch: "${env.TEST_HARNESS_BRANCH}", + url: "${env.TEST_HARNESS_REPO}" + markSafeDirectory() + + withEnv(["XKERNEL_REMOTE=${pwd()}/..", "ARCH=${arch}", + "STARRY_SKIP_BUILD=1", + "ROOTFS_CACHE_DIR=/xkernel-target/rootfs-cache", + "GUEST_CASES_TARGET_DIR=/xkernel-target/guest-cases-${arch}", + "JOBS=${env.HARNESS_JOBS}"]) { + sh label: "Run starry-test-harness ${arch}", script: """#!/bin/bash set -euo pipefail ${stageLogTeeLine(stageName)} stdbuf -oL -eL make ci-test run """ - } } } - } finally { - fixWorkspaceOwnership(stageWorkspace) } } } def runUnitTests(String arch) { - def ansiFilter = stageLogAnsiFilterPipe() - def stageTee = env.STAGE_LOG?.trim() - ? "exec > >(${ansiFilter} | tee -a '${env.STAGE_LOG}') 2>&1" - : '' - sh """#!/bin/bash + sh label: "Unit tests ${arch}", script: """#!/bin/bash set -euo pipefail -${stageTee} - -ROOTFS_VERSION=20260302 -ROOTFS_CACHE="/xkernel-target/rootfs-cache" -ROOTFS_CACHED="\${ROOTFS_CACHE}/rootfs-${arch}.img" -mkdir -p "\${ROOTFS_CACHE}" - -if [ ! -f "\${ROOTFS_CACHED}" ]; then - IMG_URL="https://gitee.com/openkylin/x-kernel-image/releases/download/\${ROOTFS_VERSION}" - curl -f -L "\${IMG_URL}/rootfs-${arch}.img.xz" -o "\${ROOTFS_CACHED}.xz" - xz -df "\${ROOTFS_CACHED}.xz" -fi -cp --reflink=auto "\${ROOTFS_CACHED}" disk.img - -TIMEOUT=480 -if [ "${arch}" = "aarch64" ]; then - TIMEOUT=481 -fi - -sed -i -e 's/WARN/__TEMP__/g' -e 's/ERROR/WARN/g' -e 's/__TEMP__/ERROR/g' .config - -set +e -timeout \${TIMEOUT} stdbuf -oL -eL make UNITTEST=y VSOCK=n NET=n run | ${ansiFilter} | tee unittest-output.log -status=\${PIPESTATUS[0]} -set -e - -if [ "\${status}" -eq 124 ]; then - echo "Unit test timed out after \${TIMEOUT}s" - exit 1 -fi - -if grep -q "UNITTEST_STATUS: TESTS_FAILED" unittest-output.log; then - echo "Unit tests failed" - exit 1 -fi - -if grep -q "UNITTEST_STATUS: ALL_TESTS_PASSED" unittest-output.log; then - exit 0 -fi - -if grep -q "panicked at" unittest-output.log; then - echo "Kernel panic detected during unit tests" - exit 1 -fi - -if grep -q "test result:.*FAILED" unittest-output.log; then - echo "Legacy unit test failure detected" - exit 1 -fi - -if grep -q "test result: ok" unittest-output.log; then - exit 0 -fi - -if [ "\${status}" -ne 0 ]; then - echo "Unit test command exited with status \${status}" - exit 1 -fi - -echo "Unable to determine test result from unit test output" -exit 1 +scripts/ci/run_unit_tests.sh '${arch}' """ } @@ -595,14 +390,15 @@ def generateCoverageHtml(String arch) { def baseDir = targetDirForArch(arch) def covInfo = "${baseDir}/${triple}/release/coverage.info" def htmlOut = "${baseDir}/${triple}/release/coverage-html" - sh """#!/bin/bash + sh label: "Generate coverage HTML ${arch}", script: """#!/bin/bash set -euo pipefail if [ ! -f "${covInfo}" ]; then echo "No coverage.info found, skipping HTML report" exit 0 fi if ! command -v genhtml &>/dev/null; then - apt-get update -qq && apt-get install -y -qq lcov >/dev/null 2>&1 + echo "genhtml not found. Please install lcov in the CI builder image." + exit 1 fi genhtml "${covInfo}" --output-directory "${htmlOut}" --title "x-kernel coverage (${arch})" echo "HTML coverage report generated at ${htmlOut}/" @@ -613,7 +409,7 @@ def copyCoverageToWorkspace(String arch) { def triple = targetTripleFor(arch) def baseDir = targetDirForArch(arch) def srcDir = "${baseDir}/${triple}/release" - sh """#!/bin/bash + sh label: "Collect coverage artifacts ${arch}", script: """#!/bin/bash set -euo pipefail mkdir -p coverage-artifacts for f in coverage-html coverage.info coverage.xml coverage.txt; do @@ -650,17 +446,17 @@ def restoreReplayGiteeEnv() { } def prepareSource() { - initStageLog('Prepare Source') + initStageLog(sourceStageName()) ws("${env.ROOT_WS}/source-cache") { def sourceWorkspace = pwd() try { deleteDir() checkoutProject() markSafeDirectory() - env.GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + env.GIT_COMMIT = sh(label: 'Resolve checked-out commit', script: 'git rev-parse HEAD', returnStdout: true).trim() echo "Checked out HEAD: ${env.GIT_COMMIT}" if (env.giteePullRequestIid?.trim()) { - checkNotDiverged('Prepare Source') + checkNotDiverged(sourceStageName()) } } finally { fixWorkspaceOwnership(sourceWorkspace) @@ -671,7 +467,7 @@ def prepareSource() { def checkNotDiverged(String stageName = '') { def targetBranch = env.giteeTargetBranch ?: env.DEFAULT_BRANCH def teeLine = stageName ? stageLogTeeLine(stageName) : '' - def result = sh(script: """#!/bin/bash + def result = sh(label: 'Check PR branch is rebased', script: """#!/bin/bash set -euo pipefail ${teeLine} git fetch origin ${targetBranch} --quiet @@ -688,7 +484,7 @@ fi } def restoreSource() { - sh "tar cf - -C '${env.ROOT_WS}/source-cache' . | tar xf -" + sh label: 'Restore source snapshot', script: "tar cf - -C '${env.ROOT_WS}/source-cache' . | tar xf -" markSafeDirectory() } @@ -717,7 +513,7 @@ def checkoutProject() { } def markSafeDirectory() { - sh '''#!/bin/bash + sh label: 'Mark Git safe.directory', script: '''#!/bin/bash set -euo pipefail dir="$(pwd)" @@ -751,7 +547,7 @@ def deleteOldCiComments() { def namespace = env.giteeTargetNamespace ?: 'openkylin' def repo = env.giteeTargetRepoName ?: 'x-kernel' withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { - def ids = sh(script: """#!/bin/bash + def ids = sh(label: 'Find old Gitee CI comments', script: """#!/bin/bash curl -sS --max-time 15 \ 'https://gitee.com/api/v5/repos/${namespace}/${repo}/pulls/${prNumber}/comments?page=1&per_page=100' \ --data-urlencode "access_token=\${GITEE_TOKEN}" | \ @@ -771,7 +567,7 @@ for c in comments: if (ids) { ids.split('\n').each { commentId -> if (commentId?.trim()) { - sh(script: """#!/bin/bash + sh(label: 'Delete old Gitee CI comment', script: """#!/bin/bash curl -sS --max-time 10 -X DELETE \ 'https://gitee.com/api/v5/repos/${namespace}/${repo}/pulls/comments/${commentId.trim()}' \ --data-urlencode "access_token=\${GITEE_TOKEN}" || true @@ -809,15 +605,39 @@ def targetDirForArch(String arch) { return "/xkernel-target/runtime-${arch}" } -def allocateFreePort() { - return sh(script: "python3 -c \"import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()\"", - returnStdout: true).trim().toInteger() +def teePortFor(String arch) { + def base = arch == 'x86_64' ? 21000 : 21100 + def build = env.BUILD_NUMBER?.toInteger() ?: 1 + return firstFreeTcpPort(base + ((build % 50) * 2), 80) +} + +def firstFreeTcpPort(int startPort, int attempts) { + return sh(label: 'Allocate TEE hostfwd port', script: """#!/bin/bash +set -euo pipefail +python3 - '${startPort}' '${attempts}' <<'PY' +import socket +import sys + +start = int(sys.argv[1]) +attempts = int(sys.argv[2]) +for port in range(start, start + attempts): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind(('127.0.0.1', port)) + except OSError: + continue + print(port) + raise SystemExit(0) +raise SystemExit(f'no free TCP port in [{start}, {start + attempts})') +PY +""", returnStdout: true).trim().toInteger() } -def allocateFreeCid() { +def teeVsockCidFor(String arch) { + def archOffset = arch == 'x86_64' ? 1 : 2 def build = env.BUILD_NUMBER?.toInteger() ?: 1 - def stage = env.STAGE_NAME?.hashCode()?.abs() ?: 0 - return 100 + ((build * 7 + stage) % 2000000) + return 100000000 + (build * 10) + archOffset } def giteeTestPass() { giteePrApi('POST', 'test', 'pass', '--data-urlencode \'force=true\'') } @@ -830,7 +650,7 @@ def resolveHeadSha() { if (env.sha?.trim()) return env.sha.trim() def sourceCache = "${env.ROOT_WS}/source-cache" if (env.ROOT_WS?.trim() && fileExists("${sourceCache}/.git")) { - return sh(script: "git -C '${sourceCache}' rev-parse HEAD", returnStdout: true).trim() + return sh(label: 'Resolve cached source commit', script: "git -C '${sourceCache}' rev-parse HEAD", returnStdout: true).trim() } return null } @@ -853,26 +673,22 @@ def giteeCheckIdsFile() { return "${ws}/gitee-check-ids.json" } +def giteeManifestFile(String action, String stageName = null) { + def ws = env.ROOT_WS?.trim() ?: env.WORKSPACE + def label = stageName?.trim() ?: action + def slug = sanitizeStageFileName("${action}-${label}") + def unique = java.util.UUID.randomUUID().toString() + return "${ws}/gitee-manifests/${slug}-${unique}.json" +} + /** 并行阶段顺序(PR 评论表格 + gitee_check_runs.py manifest) */ def ciParallelStageOrder() { - return [ - 'Clippy+Build: aarch64-crosvm-virt', - 'Clippy+Runtime: x86_64-qemu-virt', - 'Clippy+Runtime: aarch64-qemu-virt', - 'Clippy+Runtime: riscv64-qemu-virt', - 'TEE: x86_64', - 'TEE: aarch64', - ] + return ciStageNames(ciParallelStages()) } /** 串行阶段顺序(PR 评论表格 + gitee_check_runs.py manifest) */ def ciSequentialStageOrder() { - return [ - 'Prepare Source', - 'Check Environment', - 'Rustfmt', - 'Prefetch Dependencies', - ] + return ciStageNames(ciSequentialStages()) } /** @@ -919,17 +735,22 @@ def giteeCheck(String action, String stageName = null, Map ciResults = null, Map failed_stage_logs: failedStageLogs ?: [:], ] - writeJSON file: 'gitee-ci-manifest.json', json: manifest - fixWorkspaceOwnership(env.WORKSPACE) + def manifestFile = giteeManifestFile(action, stageName) + sh label: 'Prepare Gitee manifest directory', script: """#!/bin/bash +set -euo pipefail +mkdir -p '${env.ROOT_WS ?: env.WORKSPACE}/gitee-manifests' +""" + writeJSON file: manifestFile, json: manifest + fixWorkspaceOwnership(env.ROOT_WS ?: env.WORKSPACE) try { withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { - sh(script: """#!/bin/bash + sh(label: 'Update Gitee check run', script: """#!/bin/bash set -euo pipefail python3 '${scriptPath}' \\ --owner '${namespace}' \\ --repo '${repo}' \\ - --jenkins-manifest gitee-ci-manifest.json \\ + --jenkins-manifest '${manifestFile}' \\ ${prDbArg} \\ --details-url '${env.BUILD_URL ?: ''}' || { echo "WARNING: Gitee check ${action}${stageName ? " (${stageName})" : ''} failed" @@ -947,42 +768,13 @@ def giteeFinishCheckRun(String stageName, Map ciResults, Map failedStageLogs = [ } /** 保证 ciResults 有状态后再 finish,避免 Gitee 检查一直停在「进行中」。 */ -def ciFinishGiteeStage(String stageName, Map results, String failedDetail) { +def ciFinishGiteeStage(String stageName, Map results, String failedDetail = null) { if (!results[stageName]?.status) { - results[stageName] = [status: 'failed', detail: failedDetail ?: "${stageName} 缺少 CI 结果"] + results[stageName] = [status: 'failed', detail: failedDetail ?: ciFailureDetail(stageName)] echo "WARN: ${stageName} missing ciResults status before Gitee finish" } giteeFinishCheckRun(stageName, results, [:]) } - -def ciParallelStageBegin(String stageName, Map results) { - results[stageName] = [status: 'running'] -} - -/** 并行 stage 收尾:success/failure/aborted 已写入的状态不覆盖;其余标 skipped。 */ -def ciFinishParallelStage(String stageName, Map results, String skippedDetail = null) { - def st = results[stageName]?.status - def skipMsg = skippedDetail ?: '因其他并行阶段失败而中止(fail-fast)' - if (st != 'passed' && st != 'failed') { - results[stageName] = [status: 'skipped', detail: skipMsg] - } - giteeFinishCheckRun(stageName, results, [:]) -} - -/** 构建被手动中止时,未完成的并行/串行阶段在 ciResults 中标记为 skipped(供 post_finalize 收尾)。 */ -def markCiStagesAborted(Map results) { - ciStageOrder().each { name -> - def st = results[name]?.status - if (st != 'passed' && st != 'failed') { - results[name] = [status: 'skipped', detail: '构建已中止(手动停止或 fail-fast)'] - } - } -} - -/** 本 stage 的 failure { } 触发,表示该并行分支自身失败(非 fail-fast 误触发时用 aborted { } 覆盖)。 */ -def ciMarkParallelFailure(String stageName, Map results, String failedDetail) { - results[stageName] = [status: 'failed', detail: failedDetail] -} def giteeStartParallelCheckRuns() { giteeCheck('start_parallel') } def giteeEnsureCheckRunStarted(String stageName) { giteeCheck('ensure_start', stageName) } def giteeFinalizeAllCheckRuns(Map ciResults, Map failedStageLogs = [:]) { @@ -1002,7 +794,7 @@ def giteePrApi(String method, String endpoint, String label, String extraArgs) { def namespace = env.giteeTargetNamespace ?: 'openkylin' def repo = env.giteeTargetRepoName ?: 'x-kernel' withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { - sh(script: """#!/bin/bash + sh(label: "Update Gitee PR test ${label}", script: """#!/bin/bash resp=\$(curl -sS -w '\\n%{http_code}' --max-time 15 -X ${method} \ 'https://gitee.com/api/v5/repos/${namespace}/${repo}/pulls/${prNumber}/${endpoint}' \ --data-urlencode "access_token=\${GITEE_TOKEN}" ${extraArgs} 2>&1) || true @@ -1021,7 +813,7 @@ def fixWorkspaceOwnership(String workspacePath) { } withEnv(["_FIX_WS_PATH=${workspacePath}"]) { - sh '''#!/bin/bash + sh label: 'Fix workspace ownership', script: '''#!/bin/bash set -euo pipefail workspace_path="${_FIX_WS_PATH}" reference_path="$(dirname "${workspace_path}")" @@ -1055,101 +847,49 @@ def targetTripleFor(String archOrPlatform) { } def runTeeStorageTest(String arch) { - def stageName = "TEE: ${arch}" + def stageName = "TEE Storage: ${arch}" initStageLog(stageName) def result = [arch: arch, passed: 0, failed: 0, status: 'unknown', errorSnippet: ''] - def muslTarget = "${arch}-unknown-linux-musl" - def muslLinker = "${arch}-linux-musl-gcc" - def targetUpper = muslTarget.toUpperCase().replaceAll('-', '_') def teeTargetDir = "/xkernel-target/tee-${arch}" - - ws("${env.ROOT_WS}/tee-test-${arch}") { - def stageWorkspace = pwd() - def teeHostfwdPort = allocateFreePort() - def teeVsockCid = allocateFreeCid() - try { - deleteDir() - restoreSource() - - withEnv(["TARGET_DIR=${teeTargetDir}"]) { - sh """#!/bin/bash + def teeHostfwdPort = teePortFor(arch) + def teeVsockCid = teeVsockCidFor(arch) + + withCleanSourceWorkspace("tee-test-${arch}") { stageWorkspace -> + withEnv(["TARGET_DIR=${teeTargetDir}", + "HOSTFWD_PORT=${teeHostfwdPort}", + "VSOCK_CID=${teeVsockCid}"]) { + sh label: "Run TEE storage test ${arch}", script: """#!/bin/bash set -euo pipefail ${stageLogTeeLine(stageName)} - -LIBUTEE_DIR="/xkernel-target/libutee-${arch}" -mkdir -p "\${LIBUTEE_DIR}" - -echo "==> Syncing rust-libutee..." -if [ -d "\${LIBUTEE_DIR}/.git" ]; then - git -C "\${LIBUTEE_DIR}" fetch --depth 1 origin HEAD && git -C "\${LIBUTEE_DIR}" reset --hard FETCH_HEAD -else - git clone --depth 1 ${env.LIBUTEE_REPO} "\${LIBUTEE_DIR}" -fi - -echo "==> Building storage_test for ${muslTarget}..." -( cd "\${LIBUTEE_DIR}" && CC=${muslLinker} cargo +"\${AUX_RUST_TOOLCHAIN}" build --bin storage_test --release --target ${muslTarget} ) - -echo "==> Building tee_apps/sh with TEE_INIT_APPS=/tee/storage_test..." -TEE_INIT_APPS="/tee/storage_test" RUSTFLAGS= CC=${muslLinker} \\ - CARGO_TARGET_${targetUpper}_LINKER=${muslLinker} \\ - cargo build --release --target ${muslTarget} --manifest-path tee_apps/sh/Cargo.toml \\ - --target-dir "\${TARGET_DIR}/tee-apps" - -echo "==> Creating rootfs..." -env -u CARGO_BUILD_TARGET RUSTFLAGS= cargo run --release \\ - --manifest-path xtask/crate_rootfs/Cargo.toml \\ - --target-dir "\${TARGET_DIR}/crate-rootfs" -- \\ - --image disk.img --size-bytes 64M \\ - --copy "\${TARGET_DIR}/tee-apps/${muslTarget}/release/sh":/bin/sh \\ - --copy "\${LIBUTEE_DIR}/target/${muslTarget}/release/storage_test":/tee/storage_test - -echo "==> Building kernel..." -cp ${defconfigFor(arch)} .config -make build - -echo "==> Running TEE storage test..." -set +e -timeout 1200 stdbuf -oL -eL make HOSTFWD_PORT=${teeHostfwdPort} VSOCK_CID=${teeVsockCid} justrun 2>&1 | tee tee-test-output.log -QEMU_STATUS=\${PIPESTATUS[0]} -set -e - -if [ "\${QEMU_STATUS}" -eq 124 ]; then - echo "TEE_RESULT: TIMEOUT" | tee -a tee-test-output.log -elif [ "\${QEMU_STATUS}" -ne 0 ]; then - echo "TEE_RESULT: QEMU_ERROR(\${QEMU_STATUS})" | tee -a tee-test-output.log -fi +scripts/ci/run_tee_storage_test.sh '${arch}' """ - } - - def logText = readFile("${stageWorkspace}/tee-test-output.log") - result.passed = logText.split('<<< test success', -1).length - 1 - result.failed = logText.split('<<< test failed', -1).length - 1 - - if (logText.contains('TEE_RESULT: TIMEOUT')) { - result.status = 'timeout' - result.errorSnippet = "QEMU 运行超时(360s),测试未能完成\n通过: ${result.passed},失败: ${result.failed}" - } else if (logText.contains('TEE_RESULT: QEMU_ERROR')) { - result.status = 'failed' - result.errorSnippet = extractSnippet(logText, 'TEE_RESULT: QEMU_ERROR', 5) - } else if (logText.contains('panicked at')) { - result.status = 'panic' - result.errorSnippet = extractSnippet(logText, 'panicked at', 8) - } else if (result.failed > 0) { - result.status = 'failed' - result.errorSnippet = extractSnippet(logText, '<<< test failed', 5) - } else if (result.passed > 0) { - result.status = 'passed' - } else { - result.status = 'no_output' - result.errorSnippet = '未检测到任何测试输出,QEMU 可能未正常启动' - } + } - if (result.status != 'passed') { - error("TEE Storage Test ${arch}: ${result.status} (passed=${result.passed}, failed=${result.failed})") - } + def logText = readFile("${stageWorkspace}/tee-test-output.log") + result.passed = logText.split('<<< test success', -1).length - 1 + result.failed = logText.split('<<< test failed', -1).length - 1 + + if (logText.contains('TEE_RESULT: TIMEOUT')) { + result.status = 'timeout' + result.errorSnippet = "QEMU 运行超时(1200s),测试未能完成\\n通过: ${result.passed},失败: ${result.failed}" + } else if (logText.contains('TEE_RESULT: QEMU_ERROR')) { + result.status = 'failed' + result.errorSnippet = extractSnippet(logText, 'TEE_RESULT: QEMU_ERROR', 5) + } else if (logText.contains('panicked at')) { + result.status = 'panic' + result.errorSnippet = extractSnippet(logText, 'panicked at', 8) + } else if (result.failed > 0) { + result.status = 'failed' + result.errorSnippet = extractSnippet(logText, '<<< test failed', 5) + } else if (result.passed > 0) { + result.status = 'passed' + } else { + result.status = 'no_output' + result.errorSnippet = '未检测到任何测试输出,QEMU 可能未正常启动' + } - } finally { - fixWorkspaceOwnership(stageWorkspace) + if (result.status != 'passed') { + error("TEE Storage Test ${arch}: ${result.status} (passed=${result.passed}, failed=${result.failed})") } } @@ -1178,7 +918,7 @@ def collectCoverageSummary() { try { def triple = targetTripleFor(arch) def covFile = "${targetDirForArch(arch)}/${triple}/release/coverage.txt" - def content = sh(script: "cat '${covFile}' 2>/dev/null || true", returnStdout: true).trim() + def content = sh(label: "Read coverage summary ${arch}", script: "cat '${covFile}' 2>/dev/null || true", returnStdout: true).trim() if (content) { def lines = content.split('\n') def totalLine = lines.find { it.contains('TOTAL') } @@ -1232,7 +972,7 @@ def initStageLog(String stageName) { return } def logFile = stageLogFile(stageName) - sh """#!/bin/bash + sh label: "Prepare stage log: ${stageName}", script: """#!/bin/bash set -euo pipefail mkdir -p '${env.ROOT_WS}/stage-logs' : > '${logFile}' @@ -1273,7 +1013,7 @@ def archiveFailedStageLogs(Map ciResults) { return failedLogs } - sh "mkdir -p '${env.ROOT_WS}/stage-logs' stage-logs || true" + sh label: 'Prepare failed stage log archive', script: "mkdir -p '${env.ROOT_WS}/stage-logs' stage-logs || true" fixWorkspaceOwnership(env.WORKSPACE) failedStages.each { stageName -> @@ -1292,14 +1032,14 @@ def archiveFailedStageLogs(Map ciResults) { } def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { - def result = buildCiComment(ciResults, coverageSummary) + def result = buildCiComment(ciResults, coverageSummary, failedStageLogs) return [ comment: "\n${result.body}", allPassed: result.allPassed, ] } -def buildCiComment(Map results, String coverageSummary = '') { +def buildCiComment(Map results, String coverageSummary = '', Map failedStageLogs = [:]) { def stagesUrl = "${env.BUILD_URL}stages/" def stageOrder = ciStageOrder() def normalizedResults = [:] @@ -1317,7 +1057,7 @@ def buildCiComment(Map results, String coverageSummary = '') { def rows = stageOrder.collect { name -> def r = normalizedResults[name] - def icon = r.status == 'passed' ? '✅' : ((r.status == 'skipped' || r.status == 'not_run') ? '⏭' : '❌') + def icon = r.status == 'passed' ? '✅' : (r.status == 'not_run' ? '⏭' : '❌') "| ${name} | ${icon} |" }.join('\n') @@ -1341,9 +1081,10 @@ ${rows} } def errorBlocks = stageOrder.findAll { name -> - normalizedResults[name].status == 'failed' && normalizedResults[name].detail?.trim() + normalizedResults[name].status != 'passed' && + (failedStageLogs[name]?.trim() || normalizedResults[name].detail?.trim()) }.collect { name -> - def detail = normalizedResults[name].detail.take(1000) + def detail = (failedStageLogs[name]?.trim() ?: normalizedResults[name].detail).take(4000) "\n### ❌ ${name}\n\n
\n查看错误详情\n\n" + '```' + "\n${detail}\n" + '```' + "\n
" }.join('\n') diff --git a/scripts/ci/check_build_environment.sh b/scripts/ci/check_build_environment.sh new file mode 100755 index 0000000000000000000000000000000000000000..fa508fc9e2cbca20f0c0202a68ca8fb99ad2e960 --- /dev/null +++ b/scripts/ci/check_build_environment.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "==> Checking Rust build environment..." +NIGHTLY_TOOLCHAIN="${AUX_RUST_TOOLCHAIN}" + +retry() { + local attempts="$1" + shift + local i + for i in $(seq 1 "${attempts}"); do + "$@" && return 0 + if [ "${i}" = "${attempts}" ]; then + return 1 + fi + echo "Command failed, retrying (${i}/${attempts}): $*" >&2 + sleep 5 + done +} + +eval "$( +python3 <<'PY_TOOLCHAIN' +import shlex +import tomllib + +with open("rust-toolchain.toml", "rb") as f: + toolchain = tomllib.load(f)["toolchain"] + +def array(name, values): + print(f"{name}=(" + " ".join(shlex.quote(v) for v in values) + ")") + +print("XKERNEL_TOOLCHAIN=" + shlex.quote(toolchain["channel"])) +array("XKERNEL_COMPONENTS", toolchain.get("components", [])) +array("XKERNEL_TARGETS", toolchain.get("targets", [])) +PY_TOOLCHAIN +)" + +DEFAULT_EXTRA_TARGETS=( + x86_64-unknown-uefi + x86_64-unknown-linux-musl + aarch64-unknown-linux-musl + riscv64gc-unknown-linux-musl +) +NIGHTLY_TARGETS=( + x86_64-unknown-linux-musl + aarch64-unknown-linux-musl + riscv64gc-unknown-linux-musl +) + +dedup_words() { + printf '%s\n' "$@" | awk 'NF && !seen[$0]++' +} + +mapfile -t DEFAULT_TARGETS < <(dedup_words "${XKERNEL_TARGETS[@]}" "${DEFAULT_EXTRA_TARGETS[@]}") + +default_install_args=("${XKERNEL_TOOLCHAIN}" --profile minimal --no-self-update) +for component in "${XKERNEL_COMPONENTS[@]}"; do + default_install_args+=(--component "${component}") +done +for target in "${DEFAULT_TARGETS[@]}"; do + default_install_args+=(--target "${target}") +done + +nightly_install_args=("${NIGHTLY_TOOLCHAIN}" --profile minimal --component rustfmt --no-self-update) +for target in "${NIGHTLY_TARGETS[@]}"; do + nightly_install_args+=(--target "${target}") +done + +echo "==> Installing x-kernel toolchain: ${XKERNEL_TOOLCHAIN}" +retry 3 rustup toolchain install "${default_install_args[@]}" + +echo "==> Installing auxiliary nightly toolchain: ${NIGHTLY_TOOLCHAIN}" +retry 3 rustup toolchain install "${nightly_install_args[@]}" + +echo "==> Active default toolchain" +cargo --version +rustc --version +rustup show active-toolchain + +echo "==> Installed default targets" +rustup target list --installed + +echo "==> Installed nightly targets" +rustup +"${NIGHTLY_TOOLCHAIN}" target list --installed diff --git a/scripts/ci/run_tee_storage_test.sh b/scripts/ci/run_tee_storage_test.sh new file mode 100755 index 0000000000000000000000000000000000000000..8c1489b7a3149d8e98ee7146b8897752e0658af3 --- /dev/null +++ b/scripts/ci/run_tee_storage_test.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +arch="${1:?usage: run_tee_storage_test.sh }" +musl_target="${arch}-unknown-linux-musl" +musl_linker="${arch}-linux-musl-gcc" +target_upper="${musl_target^^}" +target_upper="${target_upper//-/_}" + +: "${AUX_RUST_TOOLCHAIN:?AUX_RUST_TOOLCHAIN is required}" +: "${LIBUTEE_REPO:?LIBUTEE_REPO is required}" +: "${TARGET_DIR:?TARGET_DIR is required}" +: "${HOSTFWD_PORT:?HOSTFWD_PORT is required}" +: "${VSOCK_CID:?VSOCK_CID is required}" + +libutee_dir="/xkernel-target/libutee-${arch}" +mkdir -p "${libutee_dir}" + +echo "==> Syncing rust-libutee..." +if [ -d "${libutee_dir}/.git" ]; then + git -C "${libutee_dir}" fetch --depth 1 origin HEAD + git -C "${libutee_dir}" reset --hard FETCH_HEAD +else + git clone --depth 1 "${LIBUTEE_REPO}" "${libutee_dir}" +fi + +echo "==> Building storage_test for ${musl_target}..." +( + cd "${libutee_dir}" + CC="${musl_linker}" cargo +"${AUX_RUST_TOOLCHAIN}" build \ + --bin storage_test --release --target "${musl_target}" +) + +echo "==> Building tee_apps/sh with TEE_INIT_APPS=/tee/storage_test..." +env \ + TEE_INIT_APPS="/tee/storage_test" \ + RUSTFLAGS= \ + CC="${musl_linker}" \ + "CARGO_TARGET_${target_upper}_LINKER=${musl_linker}" \ + cargo build --release --target "${musl_target}" --manifest-path tee_apps/sh/Cargo.toml \ + --target-dir "${TARGET_DIR}/tee-apps" + +echo "==> Creating rootfs..." +env -u CARGO_BUILD_TARGET RUSTFLAGS= cargo run --release \ + --manifest-path xtask/crate_rootfs/Cargo.toml \ + --target-dir "${TARGET_DIR}/crate-rootfs" -- \ + --image disk.img --size-bytes 64M \ + --copy "${TARGET_DIR}/tee-apps/${musl_target}/release/sh":/bin/sh \ + --copy "${libutee_dir}/target/${musl_target}/release/storage_test":/tee/storage_test + +echo "==> Building kernel..." +cp "platforms/${arch}-qemu-virt/defconfig" .config +make build + +echo "==> Running TEE storage test..." +set +e +timeout 1200 stdbuf -oL -eL make HOSTFWD_PORT="${HOSTFWD_PORT}" VSOCK_CID="${VSOCK_CID}" justrun 2>&1 | tee tee-test-output.log +qemu_status=${PIPESTATUS[0]} +set -e + +if [ "${qemu_status}" -eq 124 ]; then + echo "TEE_RESULT: TIMEOUT" | tee -a tee-test-output.log +elif [ "${qemu_status}" -ne 0 ]; then + echo "TEE_RESULT: QEMU_ERROR(${qemu_status})" | tee -a tee-test-output.log +fi diff --git a/scripts/ci/run_unit_tests.sh b/scripts/ci/run_unit_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..ac8f06ebe7e89596162d6a461bf034d830f8fa8b --- /dev/null +++ b/scripts/ci/run_unit_tests.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +arch="${1:?usage: run_unit_tests.sh }" + +ansi_filter() { + sed -u \ + -e 's/\x1[Bb]\[[0-9;]*[a-zA-Z]//g' \ + -e 's/\x9[Bb]\[[0-9;]*[a-zA-Z]//g' \ + -e 's/\[[0-9;]*[mK]//g' +} + +if [ -n "${STAGE_LOG:-}" ]; then + exec > >(ansi_filter | tee -a "${STAGE_LOG}") 2>&1 +fi + +ROOTFS_VERSION=20260302 +ROOTFS_CACHE="/xkernel-target/rootfs-cache" +ROOTFS_CACHED="${ROOTFS_CACHE}/rootfs-${arch}.img" +mkdir -p "${ROOTFS_CACHE}" + +if [ ! -f "${ROOTFS_CACHED}" ]; then + IMG_URL="https://gitee.com/openkylin/x-kernel-image/releases/download/${ROOTFS_VERSION}" + curl -f -L "${IMG_URL}/rootfs-${arch}.img.xz" -o "${ROOTFS_CACHED}.xz" + xz -df "${ROOTFS_CACHED}.xz" +fi +cp --reflink=auto "${ROOTFS_CACHED}" disk.img + +TIMEOUT=480 +if [ "${arch}" = "aarch64" ]; then + TIMEOUT=481 +fi + +sed -i -e 's/WARN/__TEMP__/g' -e 's/ERROR/WARN/g' -e 's/__TEMP__/ERROR/g' .config + +set +e +timeout "${TIMEOUT}" stdbuf -oL -eL make UNITTEST=y VSOCK=n NET=n run | ansi_filter | tee unittest-output.log +status=${PIPESTATUS[0]} +set -e + +if [ "${status}" -eq 124 ]; then + echo "Unit test timed out after ${TIMEOUT}s" + exit 1 +fi + +if grep -q "UNITTEST_STATUS: TESTS_FAILED" unittest-output.log; then + echo "Unit tests failed" + exit 1 +fi + +if grep -q "UNITTEST_STATUS: ALL_TESTS_PASSED" unittest-output.log; then + exit 0 +fi + +if grep -q "panicked at" unittest-output.log; then + echo "Kernel panic detected during unit tests" + exit 1 +fi + +if grep -q "test result:.*FAILED" unittest-output.log; then + echo "Legacy unit test failure detected" + exit 1 +fi + +if grep -q "test result: ok" unittest-output.log; then + exit 0 +fi + +if [ "${status}" -ne 0 ]; then + echo "Unit test command exited with status ${status}" + exit 1 +fi + +echo "Unable to determine test result from unit test output" +exit 1