diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 45f0c1f037ae19..9b937b27da4d95 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -198,7 +198,7 @@ jobs: ARCHIVE_PATH="$(Build.ArtifactStagingDirectory)/out/server/vscode-server-$TARGET.tar.gz" DIR_PATH="$(realpath ../vscode-server-$TARGET)" mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-$TARGET + build/azure-pipelines/common/reproducible-tar.sh "$ARCHIVE_PATH" -C .. vscode-server-$TARGET echo "##vso[task.setvariable variable=SERVER_DIR_PATH]$DIR_PATH" echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" env: @@ -213,7 +213,7 @@ jobs: ARCHIVE_PATH="$(Build.ArtifactStagingDirectory)/out/web/vscode-server-$TARGET-web.tar.gz" DIR_PATH="$(realpath ../vscode-server-$TARGET-web)" mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-$TARGET-web + build/azure-pipelines/common/reproducible-tar.sh "$ARCHIVE_PATH" -C .. vscode-server-$TARGET-web echo "##vso[task.setvariable variable=WEB_DIR_PATH]$DIR_PATH" echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: diff --git a/build/azure-pipelines/common/reproducible-tar.sh b/build/azure-pipelines/common/reproducible-tar.sh new file mode 100755 index 00000000000000..4b4340cedffe27 --- /dev/null +++ b/build/azure-pipelines/common/reproducible-tar.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +#--------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +#--------------------------------------------------------------------------------------------- +# +# Create a reproducible .tar.gz archive. +# +# All file mtimes, ownership and entry ordering are normalized; the gzip +# wrapper carries no embedded timestamp or filename. The committer time of +# HEAD is used as the canonical modification time (override via SOURCE_DATE_EPOCH). +# +# Usage: reproducible-tar.sh [tar args...] +# e.g. reproducible-tar.sh out/server.tar.gz -C .. vscode-server-linux-x64 + +set -e + +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [tar args...]" >&2 + exit 1 +fi + +ARCHIVE_PATH="$1" +shift + +# Exported so gzip (called by tar -z) honors it for its header mtime as well. +# Requires gzip >= 1.10 (2018); CI build images are well past that. +export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log -1 --pretty=%ct)}" + +exec tar \ + --sort=name \ + --mtime="@$SOURCE_DATE_EPOCH" \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + -czf "$ARCHIVE_PATH" \ + "$@" diff --git a/build/azure-pipelines/common/reproducible-zip.ps1 b/build/azure-pipelines/common/reproducible-zip.ps1 new file mode 100644 index 00000000000000..f415ef9aa40230 --- /dev/null +++ b/build/azure-pipelines/common/reproducible-zip.ps1 @@ -0,0 +1,54 @@ +#--------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +#--------------------------------------------------------------------------------------------- +# +# Create a reproducible .zip archive using 7-Zip on Windows. +# +# Pre-stamps every file under -TouchDir with the committer time of HEAD +# (override via $env:SOURCE_DATE_EPOCH) so that 7-Zip embeds deterministic +# timestamps in both the DOS time and the NTFS extra field. +# +# Usage: +# reproducible-zip.ps1 <7z source args...> +# +# Trailing args are passed verbatim to `7z.exe a -tzip `. +# +# Note: 7-Zip's DOS-time conversion uses Win32 APIs that follow the system +# time zone (TZ env var is ignored). Azure Pipelines Microsoft-hosted Windows +# agents are UTC by default, which is what this assumes. + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $ArchivePath, + + [Parameter(Mandatory = $true, Position = 1)] + [string] $TouchDir, + + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $ZipArgs +) + +$ErrorActionPreference = 'Stop' + +if (-not $env:SOURCE_DATE_EPOCH) { + $env:SOURCE_DATE_EPOCH = (& git log -1 --pretty=%ct).Trim() +} +$sourceDate = [DateTimeOffset]::FromUnixTimeSeconds([int64] $env:SOURCE_DATE_EPOCH).UtcDateTime + +function Set-Timestamps($item) { + try { + $item.LastWriteTimeUtc = $sourceDate + $item.CreationTimeUtc = $sourceDate + $item.LastAccessTimeUtc = $sourceDate + } catch { + # Skip entries we cannot stamp (locked files, reparse points, etc.) + } +} + +Set-Timestamps (Get-Item -LiteralPath $TouchDir -Force) +Get-ChildItem -LiteralPath $TouchDir -Recurse -Force | ForEach-Object { Set-Timestamps $_ } + +& 7z.exe a -tzip $ArchivePath @ZipArgs +exit $LASTEXITCODE diff --git a/build/azure-pipelines/common/reproducible-zip.sh b/build/azure-pipelines/common/reproducible-zip.sh new file mode 100755 index 00000000000000..539e055f91c863 --- /dev/null +++ b/build/azure-pipelines/common/reproducible-zip.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +#--------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +#--------------------------------------------------------------------------------------------- +# +# Create a reproducible .zip archive. +# +# Pre-touches every entry with the committer time of HEAD (override via +# SOURCE_DATE_EPOCH) and feeds a byte-sorted file list to zip so the archive +# is deterministic. The `-X` flag strips per-entry extra attributes (uid/gid, +# extended attributes); `-y` preserves symlinks as symlinks. +# +# Usage: reproducible-zip.sh +# +# The script `cd`s into and then runs `find -print` to build +# the entry list. is shell-expanded by the caller's shell, so: +# - pass '*' (unquoted by the caller) to include top-level entries flat, +# producing entries like 'Visual Studio Code.app/Contents/...' +# (matches the old `cd src && zip -Xry archive *` darwin-client pattern) +# - pass a literal directory name to include that directory and its +# subtree, producing entries like 'vscode-server-darwin-x64/bin/...' +# (matches the old `cd .. && zip -Xry archive name` darwin-server pattern) + +set -e + +# BSD `date -r`, `touch -t` and Info-ZIP `zip` all read TZ via localtime(); +# force UTC so the DOS time written into the zip central directory does not +# depend on the build agent's timezone. +export TZ=UTC0 + +if [ "$#" -lt 3 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +ARCHIVE_PATH="$1" +CWD="$2" +shift 2 + +# Resolve to an absolute path so the subshell `cd` below does not break it. +case "$ARCHIVE_PATH" in + /*) ;; + *) ARCHIVE_PATH="$(pwd)/$ARCHIVE_PATH" ;; +esac + +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log -1 --pretty=%ct)}" +TOUCH_DATE=$(date -r "$SOURCE_DATE_EPOCH" "+%Y%m%d%H%M.%S") + +( + cd "$CWD" + # Re-expand globs now that we are in . Callers pass patterns quoted + # (e.g. '*') so they survive the outer shell unscathed; here we eval them + # into an array so e.g. 'Visual Studio Code.app' is one element, not three. + patterns=() + for arg in "$@"; do + eval "matches=( $arg )" + patterns+=( "${matches[@]}" ) + done + if [ "${#patterns[@]}" -eq 0 ]; then + echo "Error: no entries matched in $CWD" >&2 + exit 1 + fi + # Verify each top-level entry actually exists (catches unmatched literal + # globs that fell through to the literal pattern, e.g. '*' with no match). + for entry in "${patterns[@]}"; do + if [ ! -e "$entry" ] && [ ! -L "$entry" ]; then + echo "Error: '$entry' does not exist in $CWD" >&2 + exit 1 + fi + done + find "${patterns[@]}" -exec touch -h -t "$TOUCH_DATE" {} + + find "${patterns[@]}" -print | LC_ALL=C sort | zip -X -y "$ARCHIVE_PATH" -@ +) diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index f0b468db15f9ea..145893fb7937d8 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -143,7 +143,8 @@ jobs: - script: | set -e mkdir -p $(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive - pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip * && popd + ARCHIVE_PATH="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip" + build/azure-pipelines/common/reproducible-zip.sh "$ARCHIVE_PATH" "$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)" '*' displayName: Archive build - task: UseDotNet@2 diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index 2a90c1c5dfbfb1..23796a84b65a67 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -221,7 +221,7 @@ steps: set -e ARCHIVE_PATH="$(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip" mkdir -p $(dirname $ARCHIVE_PATH) - (cd ../VSCode-darwin-$(VSCODE_ARCH) && zip -Xry $ARCHIVE_PATH *) + build/azure-pipelines/common/reproducible-zip.sh "$ARCHIVE_PATH" "../VSCode-darwin-$(VSCODE_ARCH)" '*' echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" condition: eq(variables['BUILT_CLIENT'], 'true') displayName: Package client @@ -280,7 +280,7 @@ steps: set -e ARCHIVE_PATH="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip" mkdir -p $(dirname $ARCHIVE_PATH) - (cd ../VSCode-darwin-$(VSCODE_ARCH) && zip -Xry $ARCHIVE_PATH *) + build/azure-pipelines/common/reproducible-zip.sh "$ARCHIVE_PATH" "../VSCode-darwin-$(VSCODE_ARCH)" '*' echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" condition: eq(variables['BUILT_CLIENT'], 'true') displayName: Re-package client after entitlement @@ -289,7 +289,7 @@ steps: set -e ARCHIVE_PATH=".build/darwin/server/vscode-server-darwin-$(VSCODE_ARCH).zip" mkdir -p $(dirname $ARCHIVE_PATH) - (cd .. && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH vscode-server-darwin-$(VSCODE_ARCH)) + build/azure-pipelines/common/reproducible-zip.sh "$(Build.SourcesDirectory)/$ARCHIVE_PATH" ".." "vscode-server-darwin-$(VSCODE_ARCH)" echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" displayName: Package server @@ -297,7 +297,7 @@ steps: set -e ARCHIVE_PATH=".build/darwin/server/vscode-server-darwin-$(VSCODE_ARCH)-web.zip" mkdir -p $(dirname $ARCHIVE_PATH) - (cd .. && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH vscode-server-darwin-$(VSCODE_ARCH)-web) + build/azure-pipelines/common/reproducible-zip.sh "$(Build.SourcesDirectory)/$ARCHIVE_PATH" ".." "vscode-server-darwin-$(VSCODE_ARCH)-web" echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" displayName: Package server (web) diff --git a/build/azure-pipelines/linux/build-snap.sh b/build/azure-pipelines/linux/build-snap.sh index 77adcf7035799e..a7cb406de9820c 100755 --- a/build/azure-pipelines/linux/build-snap.sh +++ b/build/azure-pipelines/linux/build-snap.sh @@ -17,7 +17,9 @@ sudo apt-get install -y curl apt-transport-https ca-certificates SNAP_ROOT="$(pwd)/.build/linux/snap/$VSCODE_ARCH" # Create snap package -BUILD_VERSION="$(date +%s)" +# SOURCE_DATE_EPOCH is provided by the outer pipeline (where git is available); +# the snapcraft container does not include git. +BUILD_VERSION="${SOURCE_DATE_EPOCH:-$(date +%s)}" SNAP_FILENAME="code-$VSCODE_QUALITY-$VSCODE_ARCH-$BUILD_VERSION.snap" SNAP_PATH="$SNAP_ROOT/$SNAP_FILENAME" case $VSCODE_ARCH in diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index 1b94a984e20c38..4cf99bf2f2d241 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -238,7 +238,7 @@ steps: - script: | set -e - tar -czf $CLIENT_PATH -C .. VSCode-linux-$(VSCODE_ARCH) + build/azure-pipelines/common/reproducible-tar.sh "$CLIENT_PATH" -C .. VSCode-linux-$(VSCODE_ARCH) env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Archive client @@ -250,7 +250,7 @@ steps: ARCHIVE_PATH=".build/linux/server/vscode-server-linux-$(VSCODE_ARCH).tar.gz" UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) + build/azure-pipelines/common/reproducible-tar.sh "$ARCHIVE_PATH" -C .. vscode-server-linux-$(VSCODE_ARCH) echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" env: @@ -263,7 +263,7 @@ steps: mv ../vscode-reh-web-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH)-web # TODO@joaomoreno ARCHIVE_PATH=".build/linux/web/vscode-server-linux-$(VSCODE_ARCH)-web.tar.gz" mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH)-web + build/azure-pipelines/common/reproducible-tar.sh "$ARCHIVE_PATH" -C .. vscode-server-linux-$(VSCODE_ARCH)-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -357,7 +357,8 @@ steps: - script: | set -e npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-snap" - sudo -E docker run -e VSCODE_ARCH -e VSCODE_QUALITY -v $(pwd):/work -w /work vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64@sha256:ab4a88c4d85e0d7a85acabba59543f7143f575bab2c0b2b07f5b77d4a7e491ff /bin/bash -c "./build/azure-pipelines/linux/build-snap.sh" + export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)" + sudo -E docker run -e VSCODE_ARCH -e VSCODE_QUALITY -e SOURCE_DATE_EPOCH -v $(pwd):/work -w /work vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64@sha256:ab4a88c4d85e0d7a85acabba59543f7143f575bab2c0b2b07f5b77d4a7e491ff /bin/bash -c "./build/azure-pipelines/linux/build-snap.sh" SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)" SNAP_EXTRACTED_PATH=$(find $SNAP_ROOT -maxdepth 1 -type d -name 'code-*') diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 8df553bc2b79d2..24f30149d417d3 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -130,7 +130,7 @@ jobs: npm run gulp vscode-web-min-ci ARCHIVE_PATH="$(Build.ArtifactStagingDirectory)/out/web/vscode-web.tar.gz" mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-web + build/azure-pipelines/common/reproducible-tar.sh "$ARCHIVE_PATH" -C .. vscode-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" diff --git a/build/azure-pipelines/win32/codesign.ts b/build/azure-pipelines/win32/codesign.ts index 1d51cb08c622e8..2057c9199aaa6b 100644 --- a/build/azure-pipelines/win32/codesign.ts +++ b/build/azure-pipelines/win32/codesign.ts @@ -45,7 +45,7 @@ async function main() { if (process.env['BUILT_CLIENT']) { printBanner('Package client'); const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}.zip`; - await $`7z.exe a -tzip ${clientArchivePath} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); + await $`build/azure-pipelines/common/reproducible-zip.ps1 ${clientArchivePath} ../VSCode-win32-${arch} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); await $`7z.exe l ${clientArchivePath}`.pipe(process.stdout); } @@ -53,7 +53,7 @@ async function main() { if (process.env['BUILT_SERVER']) { printBanner('Package server'); const serverArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}.zip`; - await $`7z.exe a -tzip ${serverArchivePath} ../vscode-server-win32-${arch}`.pipe(process.stdout); + await $`build/azure-pipelines/common/reproducible-zip.ps1 ${serverArchivePath} ../vscode-server-win32-${arch} ../vscode-server-win32-${arch}`.pipe(process.stdout); await $`7z.exe l ${serverArchivePath}`.pipe(process.stdout); } @@ -61,7 +61,7 @@ async function main() { if (process.env['BUILT_WEB']) { printBanner('Package server (web)'); const webArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}-web.zip`; - await $`7z.exe a -tzip ${webArchivePath} ../vscode-server-win32-${arch}-web`.pipe(process.stdout); + await $`build/azure-pipelines/common/reproducible-zip.ps1 ${webArchivePath} ../vscode-server-win32-${arch}-web ../vscode-server-win32-${arch}-web`.pipe(process.stdout); await $`7z.exe l ${webArchivePath}`.pipe(process.stdout); } diff --git a/build/gulpfile.vscode.linux.ts b/build/gulpfile.vscode.linux.ts index 45179160a57b86..72066847be5a33 100644 --- a/build/gulpfile.vscode.linux.ts +++ b/build/gulpfile.vscode.linux.ts @@ -17,12 +17,13 @@ import { recommendedDeps as rpmRecommendedDependencies } from './linux/rpm/dep-l import * as path from 'path'; import * as cp from 'child_process'; import { promisify } from 'util'; +import { getSourceDateEpoch } from './lib/sourceDateEpoch.ts'; const exec = promisify(cp.exec); const root = path.dirname(import.meta.dirname); const commit = getVersion(root); -const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); +const linuxPackageRevision = getSourceDateEpoch(); function getDebPackageArch(arch: string): string { switch (arch) { @@ -126,7 +127,8 @@ function buildDebPackage(arch: string) { return async () => { await exec(`chmod 755 ${product.applicationName}-${debArch}/DEBIAN/postinst ${product.applicationName}-${debArch}/DEBIAN/prerm ${product.applicationName}-${debArch}/DEBIAN/postrm`, { cwd }); await exec('mkdir -p deb', { cwd }); - await exec(`fakeroot dpkg-deb -Zxz -b ${product.applicationName}-${debArch} deb`, { cwd }); + const env = { ...process.env, SOURCE_DATE_EPOCH: String(linuxPackageRevision) }; + await exec(`fakeroot dpkg-deb -Zxz -b ${product.applicationName}-${debArch} deb`, { cwd, env }); }; } @@ -222,7 +224,8 @@ function buildRpmPackage(arch: string) { return async () => { await exec(`mkdir -p ${destination}`); - await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`); + const env = { ...process.env, SOURCE_DATE_EPOCH: String(linuxPackageRevision) }; + await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`, { env }); await exec(`cp "${rpmOut}/$(ls ${rpmOut})" ${destination}/`); }; } diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 873b3f946fd992..d06c651a264735 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -129,7 +129,7 @@ export function createAsar(folderPath: string, unpackGlobs: string[], skipGlobs: const finish = () => { { const headerPickle = pickle.createEmpty(); - headerPickle.writeString(JSON.stringify(filesystem.header)); + headerPickle.writeString(JSON.stringify(sortAsarHeader(filesystem.header))); const headerBuf = headerPickle.toBuffer(); const sizePickle = pickle.createEmpty(); @@ -164,3 +164,22 @@ export function createAsar(folderPath: string, unpackGlobs: string[], skipGlobs: } }); } + +// Recursively sorts directory entries in an asar filesystem header so that +// serialization order does not depend on the order in which files arrived in +// the input stream (which is filesystem-dependent via readdir). +function sortAsarHeader(node: T): T { + if (!node || typeof node !== 'object') { + return node; + } + const obj = node as Record; + const files = obj['files']; + if (files && typeof files === 'object') { + const sorted: Record = {}; + for (const key of Object.keys(files as Record).sort()) { + sorted[key] = sortAsarHeader((files as Record)[key]); + } + obj['files'] = sorted; + } + return node; +} diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 43b15a96c3b915..81e1e635c478f7 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -9,6 +9,7 @@ import vfs from 'vinyl-fs'; import { filter, jsonEditor } from './gulp/facade.ts'; import * as util from './util.ts'; import { getVersion } from './getVersion.ts'; +import { getSourceDate } from './sourceDateEpoch.ts'; import electron from '@vscode/gulp-electron'; type DarwinDocumentSuffix = 'document' | 'script' | 'file' | 'source code'; @@ -198,7 +199,7 @@ export const config = { urlSchemes: [product.urlProtocol] }], darwinForceDarkModeSupport: true, - darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined, + darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: getSourceDate().toISOString() })) : undefined, linuxExecutableName: product.applicationName, winIcon: 'resources/win32/code.ico', token: process.env['GITHUB_TOKEN'], diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index d5b4a0fafed0c3..26eccd03e94d86 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -521,7 +521,7 @@ export function scanBuiltinExtensions(extensionsRoot: string, exclude: string[] const scannedExtensions: IScannedBuiltinExtension[] = []; try { - const extensionsFolders = fs.readdirSync(extensionsRoot); + const extensionsFolders = fs.readdirSync(extensionsRoot).sort(); for (const extensionFolder of extensionsFolders) { if (exclude.indexOf(extensionFolder) >= 0) { continue; @@ -534,7 +534,7 @@ export function scanBuiltinExtensions(extensionsRoot: string, exclude: string[] if (!isWebExtension(packageJSON)) { continue; } - const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)).sort(); const packageNLSPath = children.filter(child => child === 'package.nls.json')[0]; const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; diff --git a/build/lib/policies/render.ts b/build/lib/policies/render.ts index 47b485d1bf0787..6dce84299be745 100644 --- a/build/lib/policies/render.ts +++ b/build/lib/policies/render.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { NlsString, LanguageTranslations, Category, Policy, Translations, ProductJson } from './types.ts'; +import { getSourceDate } from '../sourceDateEpoch.ts'; export function renderADMLString(prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string { let value: string | undefined; @@ -195,7 +196,7 @@ export function renderProfileManifest(appName: string, bundleIdentifier: string, pfm_interaction combined pfm_last_modified - ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} + ${getSourceDate().toISOString().replace(/\.\d+Z$/, 'Z')} pfm_platforms macOS diff --git a/build/lib/sourceDateEpoch.ts b/build/lib/sourceDateEpoch.ts new file mode 100644 index 00000000000000..0d6ecd132dda98 --- /dev/null +++ b/build/lib/sourceDateEpoch.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as path from 'path'; + +let cached: number | undefined; + +// Resolves the value to use for SOURCE_DATE_EPOCH (https://reproducible-builds.org/specs/source-date-epoch/). +// Returns seconds-since-epoch of the current HEAD commit, falling back to the current time. +export function getSourceDateEpoch(): number { + if (cached !== undefined) { + return cached; + } + const envValue = process.env['SOURCE_DATE_EPOCH']; + if (envValue && /^\d+$/.test(envValue)) { + cached = parseInt(envValue, 10); + return cached; + } + try { + const cwd = path.dirname(path.dirname(import.meta.dirname)); + const out = cp.execSync('git log -1 --pretty=%ct', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); + const parsed = parseInt(out, 10); + if (Number.isFinite(parsed)) { + cached = parsed; + return cached; + } + } catch { + // fall through to wall-clock fallback + } + cached = Math.floor(Date.now() / 1000); + return cached; +} + +export function getSourceDate(): Date { + return new Date(getSourceDateEpoch() * 1000); +}