diff --git a/.azure-pipelines/publish-docker.yml b/.azure-pipelines/publish-docker.yml index 3e05d3b06..f9de18cf3 100644 --- a/.azure-pipelines/publish-docker.yml +++ b/.azure-pipelines/publish-docker.yml @@ -100,6 +100,9 @@ extends: - bash: ./build.sh --build-driver displayName: "Build the driver" + - bash: echo "Node.js version of the driver: $(sed 's/.* node //' src/Playwright/.drivers/.stamp)" + displayName: "Print the Node.js version of the driver" + - task: AzureCLI@2 displayName: "Login to ACR via OIDC" inputs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b831d8e9e..a7fc4d407 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ Before building the solution, build the driver: ./build.sh --build-driver ``` -The driver comes from [microsoft/playwright](https://github.com/microsoft/playwright), at the commit in `DriverVersion` of `src/Common/Version.props`. The script clones that repository into `.upstream/` (gitignored), checks out the commit and builds the driver from it into `src/Playwright/.drivers`. Run it again when `DriverVersion` changes, for example after a pull. Do not commit, branch or edit in `.upstream/`. +The driver comes from [microsoft/playwright](https://github.com/microsoft/playwright), at the commit in `DriverVersion` of `src/Common/Version.props`. The script clones that repository into `.upstream/` (gitignored), checks out the commit and builds the driver from it into `src/Playwright/.drivers`, together with the latest Node.js release of `DriverNodeMajorVersion`. Run it again when `DriverVersion` changes, for example after a pull. Do not commit, branch or edit in `.upstream/`. #### Dotnet Format diff --git a/ROLLING.md b/ROLLING.md index b2a209011..a5b5ca58f 100644 --- a/ROLLING.md +++ b/ROLLING.md @@ -15,7 +15,7 @@ ./build.sh --roll ``` - This writes the commit and the latest Node.js LTS version to `src/Common/Version.props`, checks the commit out in `.upstream/`, builds the driver from it, generates the API and the transport channels again, and updates the browser versions in the README. The driver reports the version of `packages/playwright-core/package.json` at that commit: `1.66.0` on a release tag, `1.67.0-next` on `main`. + This writes the commit and the major version of the latest Node.js LTS to `src/Common/Version.props`, checks the commit out in `.upstream/`, builds the driver from it, generates the API and the transport channels again, and updates the browser versions in the README. Every driver build, also the ones for a release and for the Docker images, bundles the latest Node.js release of that major version. The driver reports the version of `packages/playwright-core/package.json` at that commit: `1.66.0` on a release tag, `1.67.0-next` on `main`. 2. **Port** what changed upstream into `src/Playwright/` and `src/Playwright.Tests/`. diff --git a/build.sh b/build.sh index 5e220043a..b195c5706 100755 --- a/build.sh +++ b/build.sh @@ -112,17 +112,18 @@ function roll() { local old_driver_version=$(read_driver_version < "${version_props_path}") local new_driver_version new_driver_version=$(resolve_ref "$1") - local new_node_version=$(curl -fsSL "https://nodejs.org/dist/index.json" \ - | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).find(r=>r.lts).version.slice(1)))") - if [[ -z "${new_node_version}" ]]; then - echo "Failed to determine the Node.js version for the driver" + # The driver build bundles the latest release of this major version. + local new_node_major_version=$(curl -fsSL "https://nodejs.org/dist/index.json" \ + | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).find(r=>r.lts).version.slice(1).split('.')[0]))") + if [[ -z "${new_node_major_version}" ]]; then + echo "Failed to determine the Node.js major version for the driver" exit 1 fi - echo "Rolling from microsoft/playwright ${old_driver_version} to ${new_driver_version} (Node.js ${new_node_version})..." + echo "Rolling from microsoft/playwright ${old_driver_version} to ${new_driver_version} (Node.js ${new_node_major_version})..." local xml_file_contents=$(cat "${version_props_path}") xml_file_contents=$(echo "${xml_file_contents}" | sed "s|.*|${new_driver_version}|") - xml_file_contents=$(echo "${xml_file_contents}" | sed "s|.*|${new_node_version}|") + xml_file_contents=$(echo "${xml_file_contents}" | sed "s|.*|${new_node_major_version}|") echo "${xml_file_contents}" > "${version_props_path}" # The channels generator needs the dependencies that the driver build installs. diff --git a/src/Common/Version.props b/src/Common/Version.props index a49e483dd..6707fb3df 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -3,9 +3,10 @@ 1.65.0 $(AssemblyVersion) 5ccfbd85521679ebf5ab4d836852bf1c544aa748 - - 26.13.0 + + 26 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/tools/Playwright.Tooling/DriverBuilder.cs b/src/tools/Playwright.Tooling/DriverBuilder.cs index 722f4df4e..7a6865832 100644 --- a/src/tools/Playwright.Tooling/DriverBuilder.cs +++ b/src/tools/Playwright.Tooling/DriverBuilder.cs @@ -26,10 +26,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Formats.Tar; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; @@ -40,8 +42,8 @@ namespace Playwright.Tooling; /// /// Builds the Playwright driver into src/Playwright/.drivers. The driver is the /// playwright-core package of microsoft/playwright at the commit in DriverVersion -/// (src/Common/Version.props), plus the official Node.js binaries of -/// DriverNodeVersion. This: +/// (src/Common/Version.props), plus the official Node.js binaries of the latest +/// release of DriverNodeMajorVersion. This: /// 1. clones microsoft/playwright into .upstream/ (gitignored) the first time, /// and checks out the commit; /// 2. builds it with `npm ci` and `npm run build` (once per commit); @@ -52,25 +54,30 @@ namespace Playwright.Tooling; /// .drivers/package/** - playwright-core npm package contents /// .drivers/node/LICENSE - Node.js license /// .drivers/node/<platform>/node[.exe] - per-platform Node.js binary. -/// A driver that is built is not built again until DriverVersion or -/// DriverNodeVersion changes. Do not commit, branch or edit in .upstream/: the -/// driver is built only from a checkout with no local changes. +/// A driver that is built is not built again until DriverVersion changes or +/// Node.js DriverNodeMajorVersion gets a new release. Do not commit, branch or +/// edit in .upstream/: the driver is built only from a checkout with no local changes. /// internal class DriverBuilder { private const string UpstreamUrl = "https://github.com/microsoft/playwright.git"; + private const string NodeIndexUrl = "https://nodejs.org/dist/index.json"; + private static readonly Regex _commitRegex = new("^[0-9a-f]{40}$", RegexOptions.Compiled); + private static readonly Regex _nodeReleaseRegex = new(@"^v([0-9]+\.[0-9]+\.[0-9]+)$", RegexOptions.Compiled); + // Directory names under .drivers/node/ are the platform ids that Driver.cs - // resolves at runtime; the suffixes name the Node.js download for that platform. - private static readonly (string PlatformId, string NodeSuffix)[] _platforms = new[] + // resolves at runtime; the suffixes name the Node.js download for that platform, + // and the index files name it in the "files" of a release in the Node.js index. + private static readonly (string PlatformId, string NodeSuffix, string NodeIndexFile)[] _platforms = new[] { - ("darwin-x64", "darwin-x64"), - ("darwin-arm64", "darwin-arm64"), - ("linux-x64", "linux-x64"), - ("linux-arm64", "linux-arm64"), - ("win32_x64", "win-x64"), + ("darwin-x64", "darwin-x64", "osx-x64-tar"), + ("darwin-arm64", "darwin-arm64", "osx-arm64-tar"), + ("linux-x64", "linux-x64", "linux-x64"), + ("linux-arm64", "linux-arm64", "linux-arm64"), + ("win32_x64", "win-x64", "win-x64-zip"), }; public string BasePath { get; set; } @@ -78,7 +85,10 @@ private static readonly (string PlatformId, string NodeSuffix)[] _platforms = ne /// The microsoft/playwright commit that the driver is built from. public string DriverVersion { get; set; } - public string NodeVersion { get; set; } + /// The Node.js major version that the driver bundles the latest release of. + public int NodeMajorVersion { get; set; } + + private string NodeVersion { get; set; } private string UpstreamPath => Path.Combine(BasePath, ".upstream"); @@ -88,17 +98,21 @@ internal static Task RunAsync(BuildDriverOptions o) var props = new XmlDocument(); props.Load(propsPath); string driverVersion = props.DocumentElement.SelectSingleNode("/Project/PropertyGroup/DriverVersion").FirstChild.Value.Trim(); - string nodeVersion = props.DocumentElement.SelectSingleNode("/Project/PropertyGroup/DriverNodeVersion").FirstChild.Value.Trim(); + string nodeMajorVersion = props.DocumentElement.SelectSingleNode("/Project/PropertyGroup/DriverNodeMajorVersion").FirstChild.Value.Trim(); if (!_commitRegex.IsMatch(driverVersion)) { throw new Exception($"DriverVersion in {propsPath} must be a microsoft/playwright commit (40 hex digits), got \"{driverVersion}\""); } + if (!int.TryParse(nodeMajorVersion, NumberStyles.None, CultureInfo.InvariantCulture, out int nodeMajor)) + { + throw new Exception($"DriverNodeMajorVersion in {propsPath} must be a Node.js major version such as 26, got \"{nodeMajorVersion}\""); + } return new DriverBuilder() { BasePath = Path.GetFullPath(o.BasePath), DriverVersion = driverVersion, - NodeVersion = nodeVersion, + NodeMajorVersion = nodeMajor, }.ExecuteAsync(); } @@ -259,6 +273,12 @@ private async Task ExecuteAsync() { await CheckoutUpstreamAsync().ConfigureAwait(false); + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromMinutes(5); + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"); + + NodeVersion = await ResolveNodeVersionAsync(client).ConfigureAwait(false); + var driversDirectory = new DirectoryInfo(Path.Combine(BasePath, "src", "Playwright", ".drivers")); string stamp = $"driver {DriverVersion} node {NodeVersion}"; string stampFile = Path.Combine(driversDirectory.FullName, ".stamp"); @@ -276,15 +296,11 @@ private async Task ExecuteAsync() driversDirectory.Create(); - using var client = new HttpClient(); - client.Timeout = TimeSpan.FromMinutes(5); - client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"); - var tasks = new List { BuildPlaywrightPackageAsync(driversDirectory.FullName), }; - foreach (var (platformId, nodeSuffix) in _platforms) + foreach (var (platformId, nodeSuffix, _) in _platforms) { tasks.Add(DownloadNodeAsync(client, driversDirectory.FullName, platformId, nodeSuffix)); } @@ -403,6 +419,32 @@ private async Task BuildPlaywrightPackageAsync(string driversDirectory) Console.WriteLine($"Built playwright-core at {DriverVersion}"); } + /// + /// Finds the latest release of Node.js NodeMajorVersion that has the binaries of all platforms. + /// + private async Task ResolveNodeVersionAsync(HttpClient client) + { + Version latest = null; + await WithRetriesAsync(NodeIndexUrl, async () => + { + using var stream = await GetStreamAsync(client, NodeIndexUrl).ConfigureAwait(false); + using var index = await JsonDocument.ParseAsync(stream).ConfigureAwait(false); + latest = index.RootElement.EnumerateArray() + .Where(release => _platforms.All(platform => release.GetProperty("files").EnumerateArray().Any(file => file.GetString() == platform.NodeIndexFile))) + .Select(release => _nodeReleaseRegex.Match(release.GetProperty("version").GetString())) + .Where(match => match.Success) + .Select(match => Version.Parse(match.Groups[1].Value)) + .Where(version => version.Major == NodeMajorVersion) + .Max(); + }).ConfigureAwait(false); + if (latest == null) + { + throw new Exception($"{NodeIndexUrl} has no Node.js {NodeMajorVersion} release with the binaries of all platforms"); + } + Console.WriteLine($"The latest Node.js {NodeMajorVersion} release is {latest}"); + return latest.ToString(); + } + private async Task DownloadNodeAsync(HttpClient client, string driversDirectory, string platformId, string nodeSuffix) { bool isWindows = nodeSuffix.StartsWith("win-", StringComparison.Ordinal);