Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 107 additions & 74 deletions src/web/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copyright (c) 2026, The OpenROAD Authors

load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_python//python:defs.bzl", "py_binary")
load("@rules_python//python:defs.bzl", "py_binary", "py_test")
load("//bazel:tcl_encode_or.bzl", "tcl_encode")
load("//bazel:tcl_wrap_cc.bzl", "tcl_wrap_cc")
load("//test:regression.bzl", "messages_txt")
Expand All @@ -12,6 +12,14 @@ package(
features = ["layering_check"],
)

# Read by //src/web/test:asset_list_parity_test, which holds the two build
# systems' asset lists to each other, and by the test of the report bundler.
exports_files([
"BUILD",
"CMakeLists.txt",
"src/embed_report_assets.py",
])

py_binary(
name = "embed_report_assets",
srcs = ["src/embed_report_assets.py"],
Expand All @@ -24,83 +32,68 @@ py_binary(
main = "src/embed_web_assets.py",
)

# The vendored libraries are generated files (see third-party/README.md); this
# is what catches one being edited in place instead of through the script.
py_test(
name = "vendor_lock_test",
srcs = ["third-party/update_vendor.py"],
args = ["--check"],
data = glob(
["third-party/**"],
exclude = ["third-party/update_vendor.py"],
),
legacy_create_init = 0,
main = "third-party/update_vendor.py",
)

# The report inlines these into one <script type="module">, so the order is
# what runs. Listed once and used for both srcs and the command line.
_REPORT_JS_FILES = [
"src/theme.js",
"src/display-state.js",
"src/3d-viewer-widget.js",
"src/coordinates.js",
"src/ui-utils.js",
"src/checkbox-tree-model.js",
"src/vis-tree.js",
"src/websocket-manager.js",
"src/tile-merge.js",
"src/tile-request.js",
"src/device-pixels.js",
"src/websocket-tile-layer.js",
"src/merged-tile-layer.js",
"src/display-controls.js",
"src/inspector.js",
"src/ruler.js",
"src/label-manager.js",
"src/tcl-completer.js",
"src/hierarchy-browser.js",
"src/menu-bar.js",
"src/context-menu.js",
"src/search-nav.js",
"src/capture.js",
"src/toolbar.js",
"src/image-export.js",
"src/edit-dialogs.js",
"src/title.js",
"src/clock-tree-widget.js",
"src/schematic-widget.js",
"src/charts-widget.js",
"src/timing-widget.js",
"src/drc-widget.js",
"src/selection-browser.js",
"src/main.js",
]

genrule(
name = "report_assets",
srcs = [
"src/style.css",
"src/theme.js",
"src/display-state.js",
"src/3d-viewer-widget.js",
"src/coordinates.js",
"src/ui-utils.js",
"src/checkbox-tree-model.js",
"src/vis-tree.js",
"src/websocket-manager.js",
"src/tile-merge.js",
"src/tile-request.js",
"src/device-pixels.js",
"src/websocket-tile-layer.js",
"src/merged-tile-layer.js",
"src/display-controls.js",
"src/inspector.js",
"src/ruler.js",
"src/label-manager.js",
"src/tcl-completer.js",
"src/hierarchy-browser.js",
"src/menu-bar.js",
"src/context-menu.js",
"src/search-nav.js",
"src/capture.js",
"src/toolbar.js",
"src/image-export.js",
"src/edit-dialogs.js",
"src/title.js",
"src/clock-tree-widget.js",
"src/schematic-widget.js",
"src/charts-widget.js",
"src/timing-widget.js",
"src/drc-widget.js",
"src/selection-browser.js",
"src/main.js",
],
srcs = ["src/style.css"] + _REPORT_JS_FILES,
outs = ["src/report_assets.cpp"],
cmd = "$(execpath :embed_report_assets)" +
" --output $@" +
" --css $(location src/style.css)" +
" --js $(location src/theme.js)" +
" $(location src/display-state.js)" +
" $(location src/3d-viewer-widget.js)" +
" $(location src/coordinates.js)" +
" $(location src/ui-utils.js)" +
" $(location src/checkbox-tree-model.js)" +
" $(location src/vis-tree.js)" +
" $(location src/websocket-manager.js)" +
" $(location src/tile-merge.js)" +
" $(location src/tile-request.js)" +
" $(location src/device-pixels.js)" +
" $(location src/websocket-tile-layer.js)" +
" $(location src/merged-tile-layer.js)" +
" $(location src/display-controls.js)" +
" $(location src/inspector.js)" +
" $(location src/ruler.js)" +
" $(location src/label-manager.js)" +
" $(location src/tcl-completer.js)" +
" $(location src/hierarchy-browser.js)" +
" $(location src/menu-bar.js)" +
" $(location src/context-menu.js)" +
" $(location src/search-nav.js)" +
" $(location src/capture.js)" +
" $(location src/toolbar.js)" +
" $(location src/image-export.js)" +
" $(location src/edit-dialogs.js)" +
" $(location src/title.js)" +
" $(location src/clock-tree-widget.js)" +
" $(location src/schematic-widget.js)" +
" $(location src/charts-widget.js)" +
" $(location src/timing-widget.js)" +
" $(location src/drc-widget.js)" +
" $(location src/selection-browser.js)" +
" $(location src/main.js)",
" --js" +
"".join([" $(location %s)" % f for f in _REPORT_JS_FILES]),
tools = [":embed_report_assets"],
)

Expand Down Expand Up @@ -142,14 +135,53 @@ _WEB_ASSET_FILES = [
"src/drc-widget.js",
"src/selection-browser.js",
"src/main.js",
# The vendored browser libraries. They are served from the binary like any
# other asset so the viewer never loads code from a CDN (issue #11065); see
# third-party/README.md. Their directory layout is part of the contract:
# the stylesheets reach their icons through relative urls.
"third-party/leaflet/leaflet.js",
"third-party/leaflet/leaflet.css",
"third-party/leaflet/images/layers.png",
"third-party/leaflet/images/layers-2x.png",
"third-party/leaflet/images/marker-icon.png",
"third-party/leaflet/images/marker-icon-2x.png",
"third-party/leaflet/images/marker-shadow.png",
"third-party/golden-layout/golden-layout.esm.js",
"third-party/golden-layout/css/goldenlayout-base.css",
"third-party/golden-layout/css/themes/goldenlayout-dark-theme.css",
"third-party/golden-layout/css/themes/goldenlayout-light-theme.css",
"third-party/golden-layout/img/lm_close_black.png",
"third-party/golden-layout/img/lm_close_tab_white.png",
"third-party/golden-layout/img/lm_close_white.png",
"third-party/golden-layout/img/lm_maximise_black.png",
"third-party/golden-layout/img/lm_maximise_white.png",
"third-party/golden-layout/img/lm_minimize_black.png",
"third-party/golden-layout/img/lm_minimize_white.png",
"third-party/golden-layout/img/lm_popin_black.png",
"third-party/golden-layout/img/lm_popin_white.png",
"third-party/golden-layout/img/lm_popout_black.png",
"third-party/golden-layout/img/lm_popout_white.png",
"third-party/three/three.module.min.js",
"third-party/elkjs/elk.bundled.js",
"third-party/netlistsvg/netlistsvg.bundle.js",
]

# Assets under src/ are served flat ("/main.js"); everything else keeps its
# path ("/third-party/leaflet/images/layers.png").
_WEB_ASSET_ARGS = [
"/%s=$(location %s)" % (
f[len("src/"):] if f.startswith("src/") else f,
f,
)
for f in _WEB_ASSET_FILES
]

genrule(
name = "web_assets",
srcs = _WEB_ASSET_FILES,
outs = ["src/web_assets.cpp"],
cmd = "$(execpath :embed_web_assets) --output $@ " +
" ".join(["$(location %s)" % f for f in _WEB_ASSET_FILES]),
" ".join(_WEB_ASSET_ARGS),
tools = [":embed_web_assets"],
)

Expand Down Expand Up @@ -179,7 +211,6 @@ cc_library(
"src/tile_generator.cpp",
"src/timing_report.cpp",
"src/web.cpp",
"src/web_assets.h",
"src/web_chart.cpp",
"src/web_gif.cpp",
"src/web_gif.h",
Expand All @@ -205,6 +236,8 @@ cc_library(
"src/web_viewer_hook.h",
"src/request_handler.h",
"src/clock_tree_report.h",
"src/css_inliner.h",
"src/web_assets.h",
],
copts = [
"-DBOOST_ASIO_NO_DEPRECATED",
Expand Down
44 changes: 39 additions & 5 deletions src/web/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,44 @@ set(WEB_ASSET_FILES
src/3d-viewer-widget.js
src/selection-browser.js
src/main.js
# The vendored browser libraries. They are served from the binary like any
# other asset so the viewer never loads code from a CDN (issue #11065); see
# third-party/README.md. Their directory layout is part of the contract:
# the stylesheets reach their icons through relative urls.
third-party/leaflet/leaflet.js
third-party/leaflet/leaflet.css
third-party/leaflet/images/layers.png
third-party/leaflet/images/layers-2x.png
third-party/leaflet/images/marker-icon.png
third-party/leaflet/images/marker-icon-2x.png
third-party/leaflet/images/marker-shadow.png
third-party/golden-layout/golden-layout.esm.js
third-party/golden-layout/css/goldenlayout-base.css
third-party/golden-layout/css/themes/goldenlayout-dark-theme.css
third-party/golden-layout/css/themes/goldenlayout-light-theme.css
third-party/golden-layout/img/lm_close_black.png
third-party/golden-layout/img/lm_close_tab_white.png
third-party/golden-layout/img/lm_close_white.png
third-party/golden-layout/img/lm_maximise_black.png
third-party/golden-layout/img/lm_maximise_white.png
third-party/golden-layout/img/lm_minimize_black.png
third-party/golden-layout/img/lm_minimize_white.png
third-party/golden-layout/img/lm_popin_black.png
third-party/golden-layout/img/lm_popin_white.png
third-party/golden-layout/img/lm_popout_black.png
third-party/golden-layout/img/lm_popout_white.png
third-party/three/three.module.min.js
third-party/elkjs/elk.bundled.js
third-party/netlistsvg/netlistsvg.bundle.js
)

# Prepend CMAKE_CURRENT_SOURCE_DIR to each asset file.
set(WEB_ASSET_PATHS "")
# Each asset is passed as <served path>=<file path>. Assets under src/ are
# served flat ("/main.js"); everything else keeps its path
# ("/third-party/leaflet/images/layers.png").
set(WEB_ASSET_ARGS "")
foreach(f ${WEB_ASSET_FILES})
list(APPEND WEB_ASSET_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/${f})
string(REGEX REPLACE "^src/" "" served ${f})
list(APPEND WEB_ASSET_ARGS /${served}=${CMAKE_CURRENT_SOURCE_DIR}/${f})
endforeach()

set(REPORT_ASSETS_CPP ${CMAKE_CURRENT_BINARY_DIR}/report_assets.cpp)
Expand All @@ -64,8 +96,11 @@ add_custom_command(
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/src/embed_report_assets.py
--output ${REPORT_ASSETS_CPP}
--css ${CMAKE_CURRENT_SOURCE_DIR}/src/style.css
# Same order as the report_assets genrule in BUILD: the files are
# concatenated into one scope, so the order is what runs.
--js ${CMAKE_CURRENT_SOURCE_DIR}/src/theme.js
${CMAKE_CURRENT_SOURCE_DIR}/src/display-state.js
${CMAKE_CURRENT_SOURCE_DIR}/src/3d-viewer-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/coordinates.js
${CMAKE_CURRENT_SOURCE_DIR}/src/ui-utils.js
${CMAKE_CURRENT_SOURCE_DIR}/src/checkbox-tree-model.js
Expand Down Expand Up @@ -95,7 +130,6 @@ add_custom_command(
${CMAKE_CURRENT_SOURCE_DIR}/src/charts-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/timing-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/drc-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/3d-viewer-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/selection-browser.js
${CMAKE_CURRENT_SOURCE_DIR}/src/main.js
DEPENDS
Expand All @@ -109,7 +143,7 @@ add_custom_command(
OUTPUT ${WEB_ASSETS_CPP}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/src/embed_web_assets.py
--output ${WEB_ASSETS_CPP}
${WEB_ASSET_PATHS}
${WEB_ASSET_ARGS}
DEPENDS
src/embed_web_assets.py
${WEB_ASSET_FILES}
Expand Down
6 changes: 4 additions & 2 deletions src/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,10 @@ The report includes:
- **Display controls**, hierarchy browser, clock tree, and other panels from
the live viewer (features that require server interaction show empty states).

The report requires an internet connection to load Leaflet and GoldenLayout
CSS/JS from CDN.
The report is self-contained: Leaflet, GoldenLayout and three are inlined as
`data:` URIs, so it opens from `file://` with no server and no network. The
schematic panel is not available in it -- it needs the server, and its two
libraries would add 2.8 MB to every saved file.

#### Examples

Expand Down
4 changes: 3 additions & 1 deletion src/web/src/3d-viewer-widget.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026, The OpenROAD Authors

import * as THREE from 'https://esm.sh/three@0.160.0';
// Resolved by the import map in index.html to the vendored copy under
// third-party/, which the OpenROAD binary serves (issue #11065).
import * as THREE from 'three';

import {getThemeColors, setCookie} from './theme.js';
import {downloadUrl, copyPngToClipboard} from './image-export.js';
Expand Down
46 changes: 46 additions & 0 deletions src/web/src/css_inliner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026, The OpenROAD Authors

#pragma once

#include <cstddef>
#include <string>
#include <string_view>

#include "utl/Logger.h"
#include "web_assets.h"

namespace web {

// Remembers a miss, which is what lets the saved report be refused: one that
// ships with a src="" opens to a blank page.
class ReportAssets
{
public:
explicit ReportAssets(utl::Logger* logger) : logger_(logger) {}

const EmbeddedAsset* find(std::string_view path);

bool missing() const { return missing_; }

private:
utl::Logger* logger_;
bool missing_ = false;
};

// Offset of the next url( token at or after `from`, or npos. Case-insensitive,
// and it has to be a token: "myurl(" is not one.
size_t findUrlToken(std::string_view css, size_t from);

// Resolve a stylesheet reference ("images/x.png", "../../img/y.png") against
// the directory the stylesheet is served from, into a lookup key.
std::string resolveAssetPath(std::string_view base_dir,
std::string_view reference);

// Rewrite the url() references to data: URIs; a relative one would resolve
// against wherever the report was saved.
std::string inlineStylesheetUrls(std::string_view css,
std::string_view base_dir,
ReportAssets& assets);

} // namespace web
Loading
Loading