Skip to content

Commit 3f6a1fd

Browse files
devversionjelbourn
authored andcommitted
build: enable bazel node modules symlinking [resubmit w/ fixes] (#17776)
Apparently we accidentally skipped a few post-install patches because we added the edit marker file for the first patch being applied to a file. This causes subsequent patches targeting the same file to be skipped accidentally
1 parent ef85134 commit 3f6a1fd

File tree

4 files changed

+83
-33
lines changed

4 files changed

+83
-33
lines changed

.circleci/config.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ var_2: &docker-firefox-image circleci/node:12.9.1-browsers
1313
# **Note**: When updating the beginning of the cache key, also update the cache key to match
1414
# the new cache key prefix. This allows us to take advantage of CircleCI's fallback caching.
1515
# Read more here: https://circleci.com/docs/2.0/caching/#restoring-cache.
16-
var_3: &cache_key v4-ng-mat-{{ checksum "WORKSPACE" }}-{{ checksum "yarn.lock" }}
17-
var_4: &cache_fallback_key v4-ng-mat-
16+
var_3: &cache_key v5-ng-mat-{{ checksum "tools/bazel/postinstall-patches.js" }}-{{ checksum "WORKSPACE" }}-{{ checksum "yarn.lock" }}
17+
# We want to invalidate the cache if the postinstall patches change. In order to apply new
18+
# patches, a clean version of the node modules is needed.
19+
var_4: &cache_fallback_key v5-ng-mat-{{ checksum "tools/bazel/postinstall-patches.js" }}-
1820

1921
# Settings common to each job
2022
var_5: &job_defaults

WORKSPACE

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
workspace(name = "angular_material")
1+
workspace(
2+
name = "angular_material",
3+
managed_directories = {"@npm": ["node_modules"]},
4+
)
25

36
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
47

@@ -42,20 +45,10 @@ node_repositories(
4245

4346
yarn_install(
4447
name = "npm",
45-
# Ensure that all resources are available when the "postinstall" or "preinstall" scripts
46-
# are executed in the Bazel sandbox.
47-
data = [
48-
"//:angular-tsconfig.json",
49-
"//:tools/bazel/flat_module_factory_resolution.patch",
50-
"//:tools/bazel/manifest_externs_hermeticity.patch",
51-
"//:tools/bazel/postinstall-patches.js",
52-
"//:tools/npm/check-npm.js",
53-
],
48+
# We add the postinstall patches file here so that Yarn will rerun whenever
49+
# the patches script changes.
50+
data = ["//:tools/bazel/postinstall-patches.js"],
5451
package_json = "//:package.json",
55-
# Temporarily disable node_modules symlinking until the fix for
56-
# https://github.com/bazelbuild/bazel/issues/8487 makes it into a
57-
# future Bazel release
58-
symlink_node_modules = False,
5952
yarn_lock = "//:yarn.lock",
6053
)
6154

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"yarn": ">= 1.19.1"
1414
},
1515
"scripts": {
16-
"postinstall": "node --preserve-symlinks --preserve-symlinks-main tools/bazel/postinstall-patches.js && ngcc --properties main --create-ivy-entry-points",
16+
"postinstall": "node tools/bazel/postinstall-patches.js && ngcc --properties main --create-ivy-entry-points",
1717
"build": "node ./scripts/build-packages-dist.js",
1818
"bazel:buildifier": "find . -type f \\( -name \"*.bzl\" -or -name WORKSPACE -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs buildifier -v --warnings=attr-cfg,attr-license,attr-non-empty,attr-output-default,attr-single-file,constant-glob,ctx-args,depset-iteration,depset-union,dict-concatenation,duplicated-name,filetype,git-repository,http-archive,integer-division,load,load-on-top,native-build,native-package,output-group,package-name,package-on-top,redefined-variable,repository-name,same-origin-load,string-iteration,unused-variable,unsorted-dict-items,out-of-order-load",
1919
"bazel:format-lint": "yarn -s bazel:buildifier --lint=warn --mode=check",

tools/bazel/postinstall-patches.js

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@ const shelljs = require('shelljs');
88
const path = require('path');
99
const fs = require('fs');
1010

11+
/**
12+
* Version of the post install patch. Needs to be incremented when patches
13+
* have been added or removed.
14+
*/
15+
const PATCH_VERSION = 1;
16+
1117
/** Path to the project directory. */
1218
const projectDir = path.join(__dirname, '../..');
1319

20+
/**
21+
* Object that maps a given file path to a list of patches that need to be
22+
* applied.
23+
*/
24+
const PATCHES_PER_FILE = {};
25+
1426
shelljs.set('-e');
1527
shelljs.cd(projectDir);
1628

17-
// Do not apply postinstall patches when running "postinstall" outside. The
18-
// "generate_build_file.js" file indicates that we run in Bazel managed node modules.
19-
if (!shelljs.test('-e', 'generate_build_file.js')) {
20-
return;
21-
}
22-
2329
// Workaround for https://github.com/angular/angular/issues/18810.
2430
shelljs.exec('ngc -p angular-tsconfig.json');
2531

@@ -69,7 +75,7 @@ searchAndReplace(
6975
const hasFlatModuleBundle = fs.existsSync(filePath.replace('.d.ts', '.metadata.json'));
7076
if ((filePath.includes('node_modules/') || !hasFlatModuleBundle) && $1`,
7177
'node_modules/@angular/compiler-cli/src/transformers/compiler_host.js');
72-
shelljs.cat(path.join(__dirname, './flat_module_factory_resolution.patch')).exec('patch -p0');
78+
applyPatch(path.join(__dirname, './flat_module_factory_resolution.patch'));
7379
// The three replacements below ensure that metadata files can be read by NGC and
7480
// that metadata files are collected as Bazel action inputs.
7581
searchAndReplace(
@@ -90,7 +96,7 @@ searchAndReplace(
9096
'node_modules/@angular/bazel/src/ng_module.bzl');
9197

9298
// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1208.
93-
shelljs.cat(path.join(__dirname, './manifest_externs_hermeticity.patch')).exec('patch -p0');
99+
applyPatch(path.join(__dirname, './manifest_externs_hermeticity.patch'));
94100

95101
// Workaround for using Ngcc with "--create-ivy-entry-points". This is a special
96102
// issue for our repository since we want to run Ivy by default in the module resolution,
@@ -136,18 +142,67 @@ shelljs.rm('-rf', [
136142
'node_modules/rxjs/Subscription.*',
137143
]);
138144

145+
// Apply all collected patches on a per-file basis. This is necessary because
146+
// multiple edits might apply to the same file, and we only want to mark a given
147+
// file as patched once all edits have been made.
148+
Object.keys(PATCHES_PER_FILE).forEach(filePath => {
149+
if (hasFileBeenPatched(filePath)) {
150+
console.info('File ' + filePath + ' is already patched. Skipping..');
151+
return;
152+
}
153+
154+
let content = fs.readFileSync(filePath, 'utf8');
155+
const patchFunctions = PATCHES_PER_FILE[filePath];
156+
157+
console.info(`Patching file ${filePath} with ${patchFunctions.length} edits..`);
158+
patchFunctions.forEach(patchFn => content = patchFn(content));
159+
160+
fs.writeFileSync(filePath, content, 'utf8');
161+
writePatchMarker(filePath);
162+
});
163+
139164
/**
140-
* Reads the specified file and replaces matches of the search expression
141-
* with the given replacement. Throws if no changes were made.
165+
* Applies the given patch if not done already. Throws if the patch does
166+
* not apply cleanly.
167+
*/
168+
function applyPatch(patchFile) {
169+
const patchMarkerFileName = `${path.basename(patchFile)}.patch_marker`;
170+
const patchMarkerPath = path.join(projectDir, 'node_modules/', patchMarkerFileName);
171+
172+
if (hasFileBeenPatched(patchMarkerPath)) {
173+
return;
174+
}
175+
176+
writePatchMarker(patchMarkerPath);
177+
shelljs.cat(patchFile).exec('patch -p0');
178+
}
179+
180+
/**
181+
* Schedules an edit where the specified file is read and its content replaced based on
182+
* the given search expression and corresponding replacement. Throws if no changes were made
183+
* and the patch has not been applied.
142184
*/
143185
function searchAndReplace(search, replacement, relativeFilePath) {
144186
const filePath = path.join(projectDir, relativeFilePath);
145-
const originalContent = fs.readFileSync(filePath, 'utf8');
146-
const newFileContent = originalContent.replace(search, replacement);
187+
const fileEdits = PATCHES_PER_FILE[filePath] || (PATCHES_PER_FILE[filePath] = []);
188+
189+
fileEdits.push(originalContent => {
190+
const newFileContent = originalContent.replace(search, replacement);
191+
if (originalContent === newFileContent) {
192+
throw Error(`Could not perform replacement in: ${filePath}.`);
193+
}
194+
return newFileContent;
195+
});
196+
}
147197

148-
if (originalContent === newFileContent) {
149-
throw Error(`Could not perform replacement in: ${filePath}.`);
150-
}
198+
/** Marks the specified file as patched. */
199+
function writePatchMarker(filePath) {
200+
new shelljs.ShellString(PATCH_VERSION).to(`${filePath}.patch_marker`);
201+
}
151202

152-
fs.writeFileSync(filePath, newFileContent, 'utf8');
203+
/** Checks if the given file has been patched. */
204+
function hasFileBeenPatched(filePath) {
205+
const markerFilePath = `${filePath}.patch_marker`;
206+
return shelljs.test('-e', markerFilePath) &&
207+
shelljs.cat(markerFilePath).toString().trim() === `${PATCH_VERSION}`;
153208
}

0 commit comments

Comments
 (0)