diff --git a/change/@rnw-scripts-just-task-73945eb9-b5df-4674-839a-d4ce8a79bd83.json b/change/@rnw-scripts-just-task-73945eb9-b5df-4674-839a-d4ce8a79bd83.json new file mode 100644 index 00000000000..640964f781c --- /dev/null +++ b/change/@rnw-scripts-just-task-73945eb9-b5df-4674-839a-d4ce8a79bd83.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Restore Nuget packages on yarn build", + "packageName": "@rnw-scripts/just-task", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-210a1c72-1d3f-4eca-990d-56493b316797.json b/change/react-native-windows-210a1c72-1d3f-4eca-990d-56493b316797.json new file mode 100644 index 00000000000..d5e5c2cc40b --- /dev/null +++ b/change/react-native-windows-210a1c72-1d3f-4eca-990d-56493b316797.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Restore Nuget packages on yarn build", + "packageName": "react-native-windows", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@rnw-scripts/just-task/nuget-restore-task.js b/packages/@rnw-scripts/just-task/nuget-restore-task.js new file mode 100644 index 00000000000..10a5c995be2 --- /dev/null +++ b/packages/@rnw-scripts/just-task/nuget-restore-task.js @@ -0,0 +1,231 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +const fs = require('fs'); +const path = require('path'); +const {execSync, spawnSync} = require('child_process'); +const {task} = require('just-scripts'); + +function registerNuGetRestoreTask(options) { + const config = normalizeOptions(options); + task(config.taskName, () => executeNuGetRestore(config)); +} + +function runNuGetRestore(options) { + const config = normalizeOptions(options); + executeNuGetRestore(config); +} + +function executeNuGetRestore(config) { + if (process.platform !== 'win32') { + console.log('Skipping NuGet restore on non-Windows host'); + return; + } + + if (!fs.existsSync(config.scriptPath)) { + console.warn(`NuGet restore script not found: ${config.scriptPath}`); + return; + } + + const vsDevCmd = findVsDevCmd(); + + fs.mkdirSync(config.logDirectory, {recursive: true}); + pruneRestoreLogs(config.logDirectory, config.maxLogCount); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const logPath = path.join( + config.logDirectory, + `NuGetRestore-${timestamp}.log`, + ); + + console.log( + `Restoring NuGet packages (log: ${path.relative(process.cwd(), logPath)})`, + ); + + const scriptArgs = config.scriptArguments.length + ? ` ${config.scriptArguments.join(' ')}` + : ''; + const restoreCommand = `call ${quote( + vsDevCmd, + )} && powershell -NoProfile -ExecutionPolicy Bypass -File ${quote( + config.scriptPath, + )}${scriptArgs}`; + const wrappedCommand = `${restoreCommand}`; + + writeLogHeader( + logPath, + restoreCommand, + config.workingDirectory, + config.scriptArguments, + ); + + const result = spawnSync('cmd.exe', ['/d', '/s', '/c', wrappedCommand], { + encoding: 'utf8', + windowsHide: true, + cwd: config.workingDirectory, + windowsVerbatimArguments: true, + }); + + appendProcessOutput(logPath, result); + + if (result.error) { + throw new Error(`Failed to start NuGet restore: ${result.error.message}`); + } + + if (result.status !== 0) { + const tail = readLogTail(logPath); + throw new Error( + `NuGet restore failed. See ${path.relative( + process.cwd(), + logPath, + )} for details.\n${tail}`, + ); + } + + console.log('NuGet restore completed'); +} + +function findVsDevCmd() { + const programFilesX86 = + process.env['ProgramFiles(x86)'] || process.env.ProgramFiles; + + if (!programFilesX86) { + throw new Error('Program Files directory not found in environment'); + } + + const vsWherePath = path.join( + programFilesX86, + 'Microsoft Visual Studio', + 'Installer', + 'vswhere.exe', + ); + + if (!fs.existsSync(vsWherePath)) { + throw new Error( + 'vswhere.exe not found. Install Visual Studio 2022 (or Build Tools).', + ); + } + + let installationRoot = ''; + try { + installationRoot = execSync( + `${quote(vsWherePath)} -latest -products * -requires Microsoft.Component.MSBuild -property installationPath -format value`, + {encoding: 'utf8'}, + ) + .split(/\r?\n/) + .find(Boolean); + } catch (error) { + throw new Error(`vswhere.exe failed: ${error.message}`); + } + + if (!installationRoot) { + throw new Error('No Visual Studio installation with MSBuild found'); + } + + const vsDevCmd = path.join( + installationRoot.trim(), + 'Common7', + 'Tools', + 'VsDevCmd.bat', + ); + + if (!fs.existsSync(vsDevCmd)) { + throw new Error(`VsDevCmd.bat not found at ${vsDevCmd}`); + } + + return vsDevCmd; +} + +function pruneRestoreLogs(logDir, maxLogCount) { + const files = fs + .readdirSync(logDir) + .filter(file => file.startsWith('NuGetRestore-') && file.endsWith('.log')) + .sort(); + + while (files.length > maxLogCount) { + const oldest = files.shift(); + if (oldest) { + fs.rmSync(path.join(logDir, oldest), {force: true}); + } + } +} + +function writeLogHeader(logPath, command, cwd, scriptArguments) { + const header = [ + `Command line : ${command}`, + `Working dir : ${cwd}`, + `Script args : ${ + scriptArguments && scriptArguments.length + ? scriptArguments.join(' ') + : '(none)' + }`, + `Timestamp : ${new Date().toISOString()}`, + '', + ].join('\n'); + + fs.writeFileSync(logPath, `${header}\n`, {encoding: 'utf8'}); +} + +function appendProcessOutput(logPath, result) { + let output = ''; + + if (result.stdout) { + output += result.stdout; + } + if (result.stderr) { + output += result.stderr; + } + + if (!output) { + return; + } + + fs.appendFileSync(logPath, output, {encoding: 'utf8'}); +} + +function readLogTail(logPath, lineCount = 40) { + try { + const contents = fs.readFileSync(logPath, 'utf8'); + const lines = contents.trim().split(/\r?\n/); + return lines.slice(-lineCount).join('\n'); + } catch (error) { + return `Could not read log tail: ${error.message}`; + } +} + +function normalizeOptions(options = {}) { + if (!options.scriptPath) { + throw new Error('scriptPath is required for NuGet restore task'); + } + + const resolvedScriptPath = path.resolve(options.scriptPath); + const resolvedLogDir = options.logDirectory + ? path.resolve(options.logDirectory) + : path.join(path.dirname(resolvedScriptPath), 'logs'); + + return { + taskName: options.taskName || 'restoreNuGetPackages', + scriptPath: resolvedScriptPath, + logDirectory: resolvedLogDir, + workingDirectory: options.workingDirectory + ? path.resolve(options.workingDirectory) + : path.dirname(resolvedScriptPath), + maxLogCount: options.maxLogCount ?? 5, + scriptArguments: Array.isArray(options.scriptArguments) + ? options.scriptArguments + : [], + }; +} + +function quote(value) { + return `"${value}"`; +} + +module.exports = { + registerNuGetRestoreTask, + runNuGetRestore, +}; diff --git a/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 b/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 index e4a08d3e199..cf4f26487c6 100644 --- a/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 +++ b/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 @@ -1,4 +1,5 @@ param( + [switch] $SkipLockDeletion ) [string] $RepoRoot = Resolve-Path "$PSScriptRoot\..\.." @@ -6,16 +7,17 @@ param( $StartingLocation = Get-Location Set-Location -Path $RepoRoot -try -{ - # Delete existing lock files - $existingLockFiles = (Get-ChildItem -File -Recurse -Path $RepoRoot -Filter *.lock.json) - $existingLockFiles | Foreach-Object { - Write-Host Deleting $_.FullName - Remove-Item $_.FullName +try { + if (-not $SkipLockDeletion) { + # Delete existing lock files + $existingLockFiles = (Get-ChildItem -File -Recurse -Path $RepoRoot -Filter *.lock.json) + $existingLockFiles | Foreach-Object { + Write-Host Deleting $_.FullName + Remove-Item $_.FullName + } } - $packagesSolutions = (Get-ChildItem -File -Recurse -Path $RepoRoot\packages -Filter *.sln )| Where-Object { !$_.FullName.Contains('node_modules') -and !$_.FullName.Contains('e2etest') } + $packagesSolutions = (Get-ChildItem -File -Recurse -Path $RepoRoot\packages -Filter *.sln ) | Where-Object { !$_.FullName.Contains('node_modules') -and !$_.FullName.Contains('e2etest') } $vnextSolutions = (Get-ChildItem -File -Path $RepoRoot\vnext -Filter *.sln) # Run all solutions with their defaults @@ -31,7 +33,6 @@ try & msbuild /t:Restore /p:RestoreForceEvaluate=true /p:UseExperimentalWinUI3=true $_.FullName } } -finally -{ +finally { Set-Location -Path "$StartingLocation" } \ No newline at end of file diff --git a/vnext/just-task.js b/vnext/just-task.js index 957cf343e1a..e3fb44c76b1 100644 --- a/vnext/just-task.js +++ b/vnext/just-task.js @@ -22,6 +22,9 @@ require('@rnw-scripts/just-task/flow-tasks'); const {execSync} = require('child_process'); const fs = require('fs'); +const { + registerNuGetRestoreTask, +} = require('@rnw-scripts/just-task/nuget-restore-task'); option('production'); option('clean'); @@ -71,6 +74,16 @@ task('copyReadmeAndLicenseFromRoot', () => { task('compileTsPlatformOverrides', tscTask()); +registerNuGetRestoreTask({ + taskName: 'restoreNuGetPackages', + scriptPath: path.resolve( + __dirname, + 'Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1', + ), + logDirectory: path.resolve(__dirname, 'logs'), + scriptArguments: ['-SkipLockDeletion'], +}); + task( 'build', series( @@ -79,6 +92,7 @@ task( 'copyReadmeAndLicenseFromRoot', 'layoutMSRNCxx', 'compileTsPlatformOverrides', + 'restoreNuGetPackages', 'codegen', ), );