Embed default resources in Servo applications using a servo-default-resources crate (#43182)

This PR considers the following constraints:

- Resources must be available when building servo via a published
crates.io package (i.e. no `../../../resources/<file>` file references).
- Minimal setup when writing tests (`nextest` spawns each test in its
own process, so we don't want to explicitly initialize the resource
handler for every `#[test]` fn)
- Use local resources when developing locally
- Support loading the resources from a proper resource directory if the
embedder wishes so, including via a custom mechanism, not necessarily as
files

(File) Resources that are only accessed from servoshell are out of scope
of this PR, since it mainly focusses on unblocking publishing `libservo`
to crates.io.

Baking the resources into the binary by default simplifies the setup a
lot. We already supported that before, but only for testing purposes and
explicitly not for production builds.

Using [`inventory`](https://crates.io/crates/inventory) adds a simple
way for the embedder to replace the default baked in resources, while
also keeping the test usage of baked in resources simple.

rippy.png is also referenced from image_cache - We simply duplicate it,
since the image is small, to avoid adding unnecessarily complex
solutions like adding a dedicated crate.


Testing: Covered by existing tests. [mach try
full](https://github.com/jschwe/servo/actions/runs/23811669469)
Fixes: Part of #43145

---------

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
This commit is contained in:
Jonathan Schwender
2026-04-01 09:16:28 +02:00
committed by GitHub
parent 05946163f2
commit f4877c190e
38 changed files with 234 additions and 200 deletions

View File

@@ -41,7 +41,8 @@ OriginalFilename = "servoshell.exe"
ProductName = "ServoShell"
[features]
default = ["gamepad", "servo/clipboard", "js_jit", "max_log_level", "webgpu", "webxr"]
default = ["baked-in-resources", "gamepad", "servo/clipboard", "js_jit", "max_log_level", "webgpu", "webxr"]
baked-in-resources = ["servo/baked-in-resources"]
crown = ["servo/crown"]
debugmozjs = ["servo/debugmozjs"]
gamepad = ["servo/gamepad"]

View File

@@ -12,7 +12,6 @@ use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
pub fn main() {
crate::crash_handler::install();
crate::init_crypto();
crate::resources::init();
// TODO: once log-panics is released, can this be replaced by
// log_panics::init()?

View File

@@ -43,9 +43,7 @@ impl ResourceProtocolHandler {
)));
};
let file_path = crate::resources::resources_dir_path()
.join("resource_protocol")
.join(path);
let file_path = crate::resources::resource_protocol_dir_path().join(path);
if !file_path.exists() || file_path.is_dir() {
return Box::pin(std::future::ready(Response::network_error(

View File

@@ -4,8 +4,6 @@
#![allow(non_snake_case)]
mod resources;
use std::cell::RefCell;
use std::os::raw::{c_char, c_int, c_void};
use std::ptr::NonNull;
@@ -23,7 +21,6 @@ use raw_window_handle::{
AndroidDisplayHandle, AndroidNdkWindowHandle, DisplayHandle, RawDisplayHandle, RawWindowHandle,
WindowHandle,
};
use resources::ResourceReaderInstance;
pub use servo::MediaSessionPlaybackState;
use servo::{
self, DevicePixel, EventLoopWaker, InputMethodControl, LoadStatus, MediaSessionActionType,
@@ -165,7 +162,6 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
let host = Rc::new(HostCallbacks::new(callbacks_ref, &env));
crate::init_crypto();
servo::resources::set(Box::new(ResourceReaderInstance::new()));
let (opts, mut preferences, servoshell_preferences) =
match parse_command_line_arguments(init_opts.args.as_slice()) {

View File

@@ -1,47 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::path::PathBuf;
use servo::resources::{Resource, ResourceReaderMethods};
pub(crate) struct ResourceReaderInstance;
impl ResourceReaderInstance {
pub(crate) fn new() -> ResourceReaderInstance {
ResourceReaderInstance
}
}
impl ResourceReaderMethods for ResourceReaderInstance {
fn read(&self, res: Resource) -> Vec<u8> {
Vec::from(match res {
Resource::HstsPreloadList => {
&include_bytes!("../../../../resources/hsts_preload.fstmap")[..]
},
Resource::BadCertHTML => &include_bytes!("../../../../resources/badcert.html")[..],
Resource::NetErrorHTML => &include_bytes!("../../../../resources/neterror.html")[..],
Resource::BrokenImageIcon => &include_bytes!("../../../../resources/rippy.png")[..],
Resource::DomainList => &include_bytes!("../../../../resources/public_domains.txt")[..],
Resource::BluetoothBlocklist => {
&include_bytes!("../../../../resources/gatt_blocklist.txt")[..]
},
Resource::CrashHTML => &include_bytes!("../../../../resources/crash.html")[..],
Resource::DirectoryListingHTML => {
&include_bytes!("../../../../resources/directory-listing.html")[..]
},
Resource::AboutMemoryHTML => {
&include_bytes!("../../../../resources/about-memory.html")[..]
},
Resource::DebuggerJS => &include_bytes!("../../../../resources/debugger.js")[..],
})
}
fn sandbox_access_files(&self) -> Vec<PathBuf> {
vec![]
}
fn sandbox_access_files_dirs(&self) -> Vec<PathBuf> {
vec![]
}
}

View File

@@ -102,7 +102,6 @@ use xcomponent_sys::{
use super::app::{App, AppInitOptions, EmbeddedPlatformWindow, VsyncRefreshDriver};
use super::host_trait::HostTrait;
use crate::egl::ohos::resources::ResourceReaderInstance;
use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
/// Queue length for the thread-safe function to submit URL updates to ArkTS
@@ -209,7 +208,7 @@ fn init_app(
let resource_dir = PathBuf::from(&options.resource_dir).join("servo");
debug!("Resources are located at: {:?}", resource_dir);
servo::resources::set(Box::new(ResourceReaderInstance::new(resource_dir.clone())));
resources::set_resource_dir(resource_dir.clone());
let args = options
.commandline_args

View File

@@ -3,24 +3,37 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
use servo::resources::{Resource, ResourceReaderMethods};
pub(crate) struct ResourceReaderInstance {
resource_dir: PathBuf,
pub(crate) struct ResourceReaderImpl {
resource_dir: OnceLock<PathBuf>,
}
impl ResourceReaderInstance {
pub(crate) fn new(resource_dir: PathBuf) -> Self {
assert!(resource_dir.is_dir());
Self { resource_dir }
static RESOURCE_READER: ResourceReaderImpl = ResourceReaderImpl {
resource_dir: OnceLock::new(),
};
servo::submit_resource_reader!(&RESOURCE_READER);
pub(crate) fn set_resource_dir(resource_dir: PathBuf) {
if let Err(e) = RESOURCE_READER.resource_dir.set(resource_dir) {
log::warn!("Failed to set resource dir: {:?}", e);
}
}
impl ResourceReaderMethods for ResourceReaderInstance {
impl ResourceReaderMethods for ResourceReaderImpl {
fn read(&self, res: Resource) -> Vec<u8> {
let file_path = self.resource_dir.join(res.filename());
fs::read(&file_path).expect("failed to read resource file")
let file_path = RESOURCE_READER
.resource_dir
.get()
.expect("Attempted to read resources before reader initialized")
.join("named_resources")
.join(res.filename());
fs::read(&file_path).unwrap_or_else(|e| {
panic!("Failed to read resource file: {:?}: {:?}", file_path, e);
})
}
fn sandbox_access_files(&self) -> Vec<PathBuf> {

View File

@@ -19,7 +19,7 @@ mod egl;
mod panic_hook;
mod parser;
mod prefs;
#[cfg(not(target_os = "android"))]
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
mod resources;
mod running_app_state;
mod webdriver;

View File

@@ -2,22 +2,19 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::env;
use std::path::PathBuf;
use std::sync::Mutex;
use std::{env, fs};
use cfg_if::cfg_if;
use servo::resources::{self, Resource};
static CMD_RESOURCE_DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
struct ResourceReader;
pub fn init() {
resources::set(Box::new(ResourceReader));
pub(crate) fn resource_protocol_dir_path() -> PathBuf {
resource_root_dir_path().join("resource_protocol")
}
pub(crate) fn resources_dir_path() -> PathBuf {
fn resource_root_dir_path() -> PathBuf {
// This needs to be called before the process is sandboxed
// as we only give permission to read inside the resources directory,
// not the permissions the "search" for the resources directory.
@@ -27,7 +24,7 @@ pub(crate) fn resources_dir_path() -> PathBuf {
}
// Try ./resources and ./Resources relative to the directory containing the
// canonicalised executable path, then each of its ancestors.
// canonicalized executable path, then each of its ancestors.
let mut path = env::current_exe().unwrap().canonicalize().unwrap();
while path.pop() {
path.push("resources");
@@ -51,7 +48,7 @@ pub(crate) fn resources_dir_path() -> PathBuf {
panic!("Can't find resources directory")
} else {
// Static assert that this is really a non-production build, rather
// than a failure of the build scripts production check.
// than a failure of the build script's production check.
const _: () = assert!(cfg!(servo_do_not_use_in_production));
// Try ./resources in the current directory, then each of its ancestors.
@@ -72,17 +69,3 @@ pub(crate) fn resources_dir_path() -> PathBuf {
}
}
}
impl resources::ResourceReaderMethods for ResourceReader {
fn read(&self, file: Resource) -> Vec<u8> {
let mut path = resources_dir_path();
path.push(file.filename());
fs::read(path).expect("Can't read file")
}
fn sandbox_access_files_dirs(&self) -> Vec<PathBuf> {
vec![resources_dir_path()]
}
fn sandbox_access_files(&self) -> Vec<PathBuf> {
vec![]
}
}