Skip to content

Commit 91eaa8d

Browse files
authored
[V3] Fix build indeterminism by sorting entries (#319)
1 parent 719a782 commit 91eaa8d

File tree

5 files changed

+96
-9
lines changed

5 files changed

+96
-9
lines changed

crates/atlaspack/src/requests/asset_graph_request.rs

+31-7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ struct AssetGraphBuilder {
5555
receiver: ResultReceiver,
5656
asset_request_to_asset_idx: HashMap<u64, NodeIndex>,
5757
waiting_asset_requests: HashMap<u64, HashSet<NodeIndex>>,
58+
entry_dependencies: Vec<(String, NodeIndex)>,
5859
}
5960

6061
impl AssetGraphBuilder {
@@ -71,6 +72,7 @@ impl AssetGraphBuilder {
7172
receiver,
7273
asset_request_to_asset_idx: HashMap::new(),
7374
waiting_asset_requests: HashMap::new(),
75+
entry_dependencies: Vec::new(),
7476
}
7577
}
7678

@@ -131,6 +133,22 @@ impl AssetGraphBuilder {
131133
}
132134
}
133135

136+
// Connect the entries to the root node in the graph. We do this in
137+
// alphabetical order so it's consistent between builds.
138+
//
139+
// Ideally, we wouldn't depend on edge order being consistent between builds
140+
// and instead rely on in-place sorting or similar to ensure deterministic
141+
// builds. However, as the rest of the code base (bundling, runtimes,
142+
// packaging, etc) relies on the deterministic edge order and it's very
143+
// complicated/risky to fix all the places that would be affected we'll keep it that
144+
// way for now.
145+
self
146+
.entry_dependencies
147+
.sort_by_key(|(entry, _)| entry.clone());
148+
for (_, node_index) in self.entry_dependencies.iter() {
149+
self.graph.add_edge(&self.graph.root_node(), node_index);
150+
}
151+
134152
Ok(ResultAndInvalidations {
135153
result: RequestResult::AssetGraph(AssetGraphRequestOutput { graph: self.graph }),
136154
invalidations: vec![],
@@ -192,10 +210,12 @@ impl AssetGraphBuilder {
192210
.request_context
193211
.queue_request(asset_request, self.sender.clone());
194212
} else if let Some(asset_node_index) = self.asset_request_to_asset_idx.get(&id) {
195-
// We have already completed this AssetRequest so we can connect the
196-
// Dependency to the Asset immediately
197-
self.graph.add_edge(&dependency_idx, asset_node_index);
198-
self.propagate_requested_symbols(*asset_node_index, dependency_idx);
213+
if !self.graph.has_edge(&dependency_idx, asset_node_index) {
214+
// We have already completed this AssetRequest so we can connect the
215+
// Dependency to the Asset immediately
216+
self.graph.add_edge(&dependency_idx, asset_node_index);
217+
self.propagate_requested_symbols(*asset_node_index, dependency_idx);
218+
}
199219
} else {
200220
// The AssetRequest has already been kicked off but is yet to
201221
// complete. Register this Dependency to be connected once it
@@ -282,8 +302,10 @@ impl AssetGraphBuilder {
282302
// for this AssetNode to be created
283303
if let Some(waiting) = self.waiting_asset_requests.remove(&request_id) {
284304
for dep in waiting {
285-
self.graph.add_edge(&dep, &asset_idx);
286-
self.propagate_requested_symbols(asset_idx, dep);
305+
if !self.graph.has_edge(&dep, &asset_idx) {
306+
self.graph.add_edge(&dep, &asset_idx);
307+
self.propagate_requested_symbols(asset_idx, dep);
308+
}
287309
}
288310
}
289311
}
@@ -405,10 +427,12 @@ impl AssetGraphBuilder {
405427
for target in targets {
406428
let entry =
407429
diff_paths(&entry, &self.request_context.project_root).unwrap_or_else(|| entry.clone());
430+
let entry = entry.to_str().unwrap().to_string();
408431

409-
let dependency = Dependency::entry(entry.to_str().unwrap().to_string(), target);
432+
let dependency = Dependency::entry(entry.clone(), target);
410433

411434
let dep_node = self.graph.add_entry_dependency(dependency.clone());
435+
self.entry_dependencies.push((entry, dep_node));
412436

413437
let request = PathRequest {
414438
dependency: Arc::new(dependency),

crates/atlaspack_core/src/asset_graph/asset_graph.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ impl AssetGraph {
175175
pub fn add_entry_dependency(&mut self, dependency: Dependency) -> NodeIndex {
176176
let is_library = dependency.env.is_library;
177177
let dependency_idx = self.add_dependency(dependency);
178-
self.add_edge(&self.root_node_index.clone(), &dependency_idx);
179178

180179
if is_library {
181180
if let Some(dependency_node) = self.get_dependency_node_mut(&dependency_idx) {
@@ -186,6 +185,10 @@ impl AssetGraph {
186185
dependency_idx
187186
}
188187

188+
pub fn has_edge(&mut self, from_idx: &NodeIndex, to_idx: &NodeIndex) -> bool {
189+
self.graph.contains_edge(*from_idx, *to_idx)
190+
}
191+
189192
pub fn add_edge(&mut self, from_idx: &NodeIndex, to_idx: &NodeIndex) {
190193
self.graph.add_edge(*from_idx, *to_idx, ());
191194
}

crates/node-bindings/src/atlaspack/serialize_asset_graph.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use atlaspack_core::types::{Asset, Dependency};
1212
/// nodes: Array<JsBuffer>,
1313
/// }
1414
/// ```
15+
#[tracing::instrument(level = "info", skip_all)]
1516
pub fn serialize_asset_graph(env: &Env, asset_graph: &AssetGraph) -> anyhow::Result<JsObject> {
1617
// Serialize graph nodes in parallel
1718
let nodes = asset_graph

packages/core/integration-tests/test/bundler.js

+56
Original file line numberDiff line numberDiff line change
@@ -2341,4 +2341,60 @@ describe('bundler', function () {
23412341
await run(splitBundle);
23422342
},
23432343
);
2344+
2345+
it('should produce deterministic builds with multiple entries', async () => {
2346+
await fsFixture(overlayFS, __dirname)`
2347+
deterministic-builds
2348+
shared-one.js:
2349+
export const one = 'one';
2350+
shared-two.js:
2351+
export const two = 'two';
2352+
one.js:
2353+
import {one} from './shared-one';
2354+
import {two} from './shared-two';
2355+
sideEffectNoop(one + two);
2356+
entry-one.html:
2357+
<script type="module" src="./one.js" />
2358+
two.js:
2359+
import {two} from './shared-two';
2360+
import {one} from './shared-one';
2361+
sideEffectNoop(two + one);
2362+
entry-two.html:
2363+
<script type="module" src="./two.js" />
2364+
package.json:
2365+
{
2366+
"@atlaspack/bundler-default": {
2367+
"minBundleSize": 0,
2368+
"manualSharedBundles": [{
2369+
"name": "shared",
2370+
"assets": ["shared*.js"]
2371+
}]
2372+
}
2373+
}
2374+
yarn.lock:
2375+
`;
2376+
2377+
let build = () =>
2378+
bundle(
2379+
[
2380+
path.join(__dirname, 'deterministic-builds/entry-one.html'),
2381+
path.join(__dirname, 'deterministic-builds/entry-two.html'),
2382+
],
2383+
{
2384+
inputFS: overlayFS,
2385+
},
2386+
).then((b) =>
2387+
b
2388+
.getBundles()
2389+
.map((b) => b.filePath)
2390+
.sort(),
2391+
);
2392+
2393+
let bundles = await build();
2394+
2395+
for (let i = 0; i < 10; i++) {
2396+
let compareBundles = await build();
2397+
assert.deepEqual(bundles, compareBundles);
2398+
}
2399+
});
23442400
});

packages/core/integration-tests/test/scope-hoisting.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -6177,8 +6177,11 @@ describe('scope hoisting', function () {
61776177
let contents = await outputFS.readFile(indexBundle.filePath, 'utf8');
61786178
assert(contents.includes('$parcel$global.rwr('));
61796179

6180+
let indexHtmlBundle = nullthrows(
6181+
b.getBundles().find((b) => /index.*\.html/.test(b.filePath)),
6182+
);
61806183
let result;
6181-
await run(b, {
6184+
await runBundle(b, indexHtmlBundle, {
61826185
result: (r) => {
61836186
result = r;
61846187
},

0 commit comments

Comments
 (0)