librsvg source for verification 2026-05-22

This commit is contained in:
2026-05-22 16:45:08 +08:00
commit 75af7ac721
2138 changed files with 161177 additions and 0 deletions

20
ci/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
[package]
name = "ci"
license.workspace = true
edition.workspace = true
rust-version.workspace = true
# Due to the unconventional layout of files
autobins = false
[dependencies]
clap.workspace = true
regex.workspace = true
rsvg_convert = { path = "../rsvg_convert" }
[[bin]]
name = "check-rsvg-convert-options"
path = "check_rsvg_convert_options.rs"

111
ci/build-dependencies.sh Normal file
View File

@@ -0,0 +1,111 @@
#!/bin/bash
# The following is to disable "info" warnings for the unquoted instandes of $MESON_FLAGS
# in the meson invocations below. We want the shell to actually split $MESON_FLAGS by spaces.
# shellcheck disable=SC2086
#
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
set -o errexit -o pipefail -o noclobber -o nounset
# See the versions here:
# https://gitlab.gnome.org/GNOME/gnome-build-meta/-/tree/gnome-47/elements/sdk (or later versions)
# https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/tree/master/elements/components
FREETYPE2_TAG="VER-2-13-3"
FONTCONFIG_TAG="2.17.1"
CAIRO_TAG="1.18.4"
HARFBUZZ_TAG="12.3.2"
PANGO_TAG="1.57.0"
LIBXML2_TAG="v2.15.1"
GDK_PIXBUF_TAG="2.44.5"
PARSED=$(getopt --options '' --longoptions 'prefix:,meson-flags:' --name "$0" -- "$@")
eval set -- "$PARSED"
unset PARSED
PREFIX=
MESON_FLAGS=
while true; do
case "$1" in
'--prefix')
PREFIX=$2
shift 2
;;
'--meson-flags')
MESON_FLAGS=$2
shift 2
;;
'--')
shift
break
;;
*)
echo "Programming error"
exit 3
;;
esac
done
if [ -z "$PREFIX" ]; then
echo "please specify a --prefix"
exit 1
fi
# The following assumes that $PREFIX has been set
source ci/setup-dependencies-env.sh
cd ..
git clone --depth 1 --branch $FREETYPE2_TAG https://gitlab.freedesktop.org/freetype/freetype
cd freetype
meson setup _build --prefix "$PREFIX" -Dharfbuzz=disabled $MESON_FLAGS
meson compile -C _build
meson install -C _build
cd ..
git clone --depth 1 --branch $FONTCONFIG_TAG https://gitlab.freedesktop.org/fontconfig/fontconfig
cd fontconfig
meson setup _build --prefix "$PREFIX" $MESON_FLAGS
meson compile -C _build
meson install -C _build
cd ..
git clone --depth 1 --branch $CAIRO_TAG https://gitlab.freedesktop.org/cairo/cairo
cd cairo
meson setup _build --prefix "$PREFIX" $MESON_FLAGS
meson compile -C _build
meson install -C _build
cd ..
git clone --depth 1 --branch $HARFBUZZ_TAG https://github.com/harfbuzz/harfbuzz
cd harfbuzz
meson setup _build --prefix "$PREFIX" $MESON_FLAGS
meson compile -C _build
meson install -C _build
cd ..
git clone --depth 1 --branch $PANGO_TAG https://gitlab.gnome.org/GNOME/pango
cd pango
meson setup _build --prefix "$PREFIX" $MESON_FLAGS
meson compile -C _build
meson install -C _build
cd ..
git clone --depth 1 --branch $LIBXML2_TAG https://gitlab.gnome.org/GNOME/libxml2
cd libxml2
mkdir _build
cd _build
../autogen.sh --prefix "$PREFIX" --libdir "$PREFIX"/lib64 --without-python
make
make install
cd ..
git clone --depth 1 --branch $GDK_PIXBUF_TAG https://gitlab.gnome.org/GNOME/gdk-pixbuf
cd gdk-pixbuf
meson setup _build --prefix "$PREFIX" -Dman=false -Dglycin=disabled $MESON_FLAGS
meson compile -C _build
meson install -C _build

29
ci/build-with-coverage.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -eux -o pipefail
clang_version=$(clang --version | head -n 1 | cut -d' ' -f 3 | cut -d'.' -f 1)
clang_libraries_path="/usr/lib64/clang/$clang_version/lib/linux"
clang_profile_lib="clang_rt.profile-x86_64"
if [ ! -d "$clang_libraries_path" ]
then
echo "Expected clang libraries (for $clang_profile_lib) to be in $clang_libraries_path"
echo "but that directory does not exist. Please adjust the build-with-coverage.sh script."
exit 1
fi
# Mixed gcc and Rust/LLVM coverage for the C API tests:
# https://searchfox.org/mozilla-central/source/browser/config/mozconfigs/linux64/code-coverage#15
export CC="clang"
export RUSTDOCFLAGS="-C instrument-coverage"
LLVM_PROFILE_FILE="$(pwd)/coverage-profiles/coverage-%p-%m.profraw"
export LLVM_PROFILE_FILE
export RUSTC_BOOTSTRAP="1" # hack to make unstable options work on the non-nightly compiler
export RUSTFLAGS="-C instrument-coverage -Z coverage-options=condition -Ccodegen-units=1 -Clink-dead-code -Coverflow-checks=off"
# meson setup _build -Db_coverage=true -Dauto_features=disabled -Dpixbuf{,-loader}=enabled --buildtype=debugoptimized
# meson compile -C _build
# meson test -C _build --maxfail 0 --print-errorlogs
cargo test -- --include-ignored

View File

@@ -0,0 +1,106 @@
# Checks that the example Cargo.toml snippet from rsvg/src/lib.rs has the same versions for
# dependencies that librsvg uses during compilation.
import sys
import toml
# Looks for a crate version in the 'dependencies' section of a TOML document, either of these:
#
# [dependencies]
# foo = "1.2.3"
# bar = { version = "4.5.6", features=["something", "else", "here"]
def get_crate_version(toml_doc, crate_name):
if 'dependencies' in toml_doc:
crate_decl = toml_doc['dependencies'][crate_name]
else:
crate_decl = toml_doc['workspace']['dependencies'][crate_name]
if isinstance(crate_decl, str):
version = crate_decl
else:
version = crate_decl['version']
return version
# Given a Rust file that has a toplevel comment somewhere like
#
# //! ```toml
# //! [dependencies]
# //! librsvg = "2.57.0-beta.2"
# //! cairo-rs = "0.18"
# //! gio = "0.18" # only if you need streams
# //! ```
#
# extracts just the TOML as a string, without the //! prefix.
def find_toml_in_rust_toplevel_docs(lines):
found_start = False
start_index = 0
end_index = 0
for (i, line) in enumerate(lines):
if not found_start and line.startswith('//! ```toml'):
found_start = True
start_index = i
end_index = i
elif found_start and line.startswith('//! ```'):
end_index = i
break
if not found_start:
raise Exception(
"did not find start of ```toml block in the toplevel documentation comments"
)
snippet = lines[(start_index + 1):end_index]
without_comment = [s.removeprefix('//! ') for s in snippet]
return "".join(without_comment)
def check_dependency_version(cargo_toml_filename, cargo_toml, other_filename, other_toml,
dependency_name):
dep_in_cargo_toml = get_crate_version(cargo_toml, dependency_name)
dep_in_other = get_crate_version(other_toml, dependency_name)
if dep_in_cargo_toml != dep_in_other:
raise Exception(
f"""{dependency_name} version in {cargo_toml_filename} is {dep_in_cargo_toml} but
is referenced in {other_filename} as {dep_in_other}"""
)
def check():
cargo_toml = toml.load('rsvg/Cargo.toml')
librsvg_version = cargo_toml['package']['version']
example_file = open('rsvg/src/lib.rs')
example_contents = example_file.readlines()
example_toml_str = find_toml_in_rust_toplevel_docs(example_contents)
example_toml = toml.loads(example_toml_str)
example_version = get_crate_version(example_toml, 'librsvg')
if librsvg_version != example_version:
raise Exception(
f"""librsvg version in rsvg/Cargo.toml is {librsvg_version} but is referenced as
{example_version} in rsvg/src/lib.rs"""
)
DEPENDENCIES = [
'cairo-rs',
'gio',
]
cargo_toml = toml.load('Cargo.toml')
for dependency_name in DEPENDENCIES:
check_dependency_version(
'Cargo.toml',
cargo_toml,
'rsvg/src/lib.rs',
example_toml,
dependency_name
)
print("Dependency versions match in rsvg/src/lib.rs. All good!", file=sys.stderr)
if __name__ == '__main__':
check()

6
ci/check_docs_links.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -eu
mkdir -p public/devel-docs-check
sphinx-build -b linkcheck devel-docs public/devel-docs-check

View File

@@ -0,0 +1,39 @@
# This script checks that the project's version is the same in a few files where it must appear.
import sys
import toml
from utils import get_project_version_str
def get_cargo_toml_version():
doc = toml.load('Cargo.toml')
return doc['workspace']['package']['version']
def get_doc_version():
doc = toml.load('doc/librsvg.toml')
return doc['library']['version']
def main():
versions = [
['meson.build', get_project_version_str()],
['Cargo.toml', get_cargo_toml_version()],
['doc/librsvg.toml', get_doc_version()],
]
all_the_same = True
for filename, version in versions[1:]:
if version != versions[0][1]:
all_the_same = False
if not all_the_same:
print('Version numbers do not match, please fix them!\n', file=sys.stderr)
for filename, version in versions:
print(f'{filename}: {version}', file=sys.stderr)
sys.exit(1)
print('Versions number match. All good!', file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,66 @@
# Checks that the version of the librsvg public crate matches the version for the GNOME library.
#
# For stable releases:
# - GNOME: 2.57.2
# - crate: 2.57.2
#
# For development relases, .9x vs. -beta.x
# - GNOME: 2.57.90
# - crate: 2.58.0-beta.0
import semver
import sys
import toml
from utils import get_project_version_str
def gen_crate_version_from_project_version(v):
if v.patch < 90:
# stable release, just return it
return v
elif v.patch >= 90 and v.patch < 99:
# development release, mangle it for semver
patch_level = v.patch - 90
beta = f'beta.{patch_level}'
return v.bump_minor().replace(prerelease = beta)
else:
raise Exception("don't know what to do with patch versions larger than 99")
def check_crate_version(project_version_str, crate_version_str):
# GNOME only likes x.y.z versions
main_version = semver.Version.parse(project_version_str)
assert main_version.major is not None
assert main_version.minor is not None
assert main_version.patch is not None
assert main_version.prerelease is None
assert main_version.build is None
crate_version = semver.Version.parse(crate_version_str)
if gen_crate_version_from_project_version(main_version) != crate_version:
raise Exception(
f'meson.build version {main_version} does not match rsvg crate version {crate_version}'
)
def test_stable():
a = semver.Version.parse('2.56.3')
assert gen_crate_version_from_project_version(a) == a
def test_development():
a = semver.Version.parse('2.56.90')
assert gen_crate_version_from_project_version(a) == semver.Version.parse('2.57.0-beta.0')
a = semver.Version.parse('2.57.93')
assert gen_crate_version_from_project_version(a) == semver.Version.parse('2.58.0-beta.3')
def main():
project_version_str = get_project_version_str()
doc = toml.load('rsvg/Cargo.toml')
crate_version_str = doc['package']['version']
check_crate_version(project_version_str, crate_version_str)
print('Versions number match. All good!', file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,731 @@
//! This crate tests that rsvg-convert's man page fully and properly documents its options.
//! It uses/enforces the format specified in the "OPTIONS" section of `rsvg-convert.rst`.
// Allow references to `mut` statics since there's no multithreading.
//
// If this ever changes and mutable statics are used in any function possibly
// executed in multiple threads, please do the needful (maybe use `RefCell`).
#![allow(static_mut_refs)]
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::iter::Peekable;
use std::mem::MaybeUninit;
use std::ops::RangeInclusive;
use std::process::ExitCode;
use clap::builder::{PossibleValue, ValueRange};
use clap::{Arg, ArgAction};
use regex::Regex;
use self::Error::*;
type UnitResult<E> = Result<(), E>;
// These are statics to avoid recompiling Regex patterns every time the functions in
// which they're used are called, since those functions are called multile times by
// design.
//
// Initialized and dropped in `main()`.
static mut VALUE_NAME_SEGMENT_RE: MaybeUninit<Regex> = MaybeUninit::uninit();
static mut VALUE_NAME_RE: MaybeUninit<Regex> = MaybeUninit::uninit();
static mut POSSIBLE_VALUES_RE: MaybeUninit<Regex> = MaybeUninit::uninit();
#[derive(Debug)]
enum Error {
InvalidFormat {
message: String,
string: String,
causes: &'static [&'static str],
},
UnspecifiedOption(String),
// Short name
MismacthedShortNames {
option: String,
documented: char,
specified: char,
},
UndocumentedShortName {
option: String,
short: char,
},
UnspecifiedShortName {
option: String,
short: char,
},
// Value name
InvalidValueName {
option: String,
value_name: String,
},
MismacthedValueNames {
option: String,
documented: String,
specified: String,
},
UndocumentedValueName {
option: String,
value_name: String,
},
UnspecifiedValueName {
option: String,
value_name: String,
},
// Description
InvalidDescriptionIndentation {
option: String,
line_no: i32,
indent: String,
expected: String,
},
NoDescription {
option: String,
},
NoValueDescription {
option: String,
required_because: &'static str,
},
// // Possible values
InvalidPossibleValues {
option: String,
line_range: RangeInclusive<i32>,
values: Vec<String>,
},
MismatchedPossibleValues {
option: String,
line_range: RangeInclusive<i32>,
documented: HashSet<String>,
specified: HashSet<String>,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidFormat {
message,
string,
causes,
} => {
f.write_str(message)?;
write!(f, "\n string: {string:?}")?;
if !causes.is_empty() {
f.write_str("\n possible causes:")?;
for cause in *causes {
write!(f, "\n - {cause}")?;
}
}
}
UnspecifiedOption(option) => write!(f, "`--{option}` is documented but not specified")?,
// Short name
MismacthedShortNames {
option,
documented,
specified,
} => write!(
f,
"`--{option}` specifies short name `-{specified}` but documents `-{documented}`",
)?,
UndocumentedShortName { option, short } => write!(
f,
"`--{option}` specifies short name `-{short}` but documents none",
)?,
UnspecifiedShortName { option, short } => write!(
f,
"`--{option}` specifies no short name but documents `-{short}`",
)?,
// Value name
InvalidValueName { option, value_name } => {
write!(f, "Invalid value name {value_name:?} for `--{option}`")?;
f.write_str(
"\
\n possible causes:\
\n - contains a non-alphabetic character other than '-', '.', '_'\
\n - contains any of the allowed non-alphabetic characters in succession\
\n - doesn't start with an alphabetic character\
\n - doesn't end with an alphabetic character",
)?;
}
MismacthedValueNames {
option,
documented,
specified,
} => write!(
f,
"`--{option}` specifies value name {specified:?} but documents {documented:?}",
)?,
UndocumentedValueName { option, value_name } => write!(
f,
"`--{option}` specifies value name {value_name:?} but documents none",
)?,
UnspecifiedValueName { option, value_name } => write!(
f,
"`--{option}` specifies no value name but documents {value_name:?}",
)?,
// Description
InvalidDescriptionIndentation {
option,
line_no,
indent,
expected,
} => {
write!(f, "Invalid indentation in the description of `--{option}`")?;
write!(f, "\n line no: {line_no}")?;
write!(f, "\n indent: {indent:?}")?;
write!(f, "\n expected: {expected:?}")?;
}
NoDescription { option } => write!(f, "`--{option}` has no description")?,
NoValueDescription {
option,
required_because,
} => {
write!(f, "`--{option}` has no value description")?;
write!(f, "\n required because {required_because}")?;
}
// // Possible values
InvalidPossibleValues {
option,
line_range,
values,
} => {
write!(f, "Invalid possible values {values:?} for `--{option}`")?;
f.write_str("\n these values contain whitespace")?;
write!(f, "\n possible values documented on lines {line_range:?}")?;
}
MismatchedPossibleValues {
option,
line_range,
documented,
specified,
} => {
let undocumented = specified - documented;
let unspecified = documented - specified;
write!(f, "Mismatched possible values for `--{option}`")?;
if !undocumented.is_empty() {
write!(f, "\n specified but not documented: {undocumented:?}")?;
}
if !unspecified.is_empty() {
write!(f, "\n documented but not specified: {unspecified:?}")?;
}
write!(f, "\n possible values documented on lines {line_range:?}")?;
}
}
Ok(())
}
}
fn main() -> ExitCode {
let command = rsvg_convert::build_cli();
let mut man_page = BufReader::new(File::open("rsvg-convert.rst").unwrap());
let mut n_errors = 0;
let mut options: HashMap<&str, &Arg> = HashMap::new();
for option in command.get_opts() {
options.insert(option.get_long().unwrap(), option);
}
// Initialize static `Regex`s (see the comment above the static items).
unsafe {
VALUE_NAME_SEGMENT_RE.write(Regex::new(r"^\*(.+)\*$").unwrap());
VALUE_NAME_RE.write(Regex::new(r"(?i)^[a-z]+(?:[-._][a-z]+)*$").unwrap());
POSSIBLE_VALUES_RE.write(
Regex::new(r"^Possible values are ((?:\s*``[^`]+``\s*, )+\s*``[^`]+``)\.$").unwrap(),
);
}
if let Err(errors) = check_options(&mut options, &mut man_page) {
n_errors += errors.len();
for (line_no, error) in errors {
eprintln!("line {line_no}: {error}\n");
}
}
if !options.is_empty() {
n_errors += options.len();
for long_name in options.keys() {
eprintln!("`--{long_name}` is specified but not documented\n");
}
}
// Drop static `Regex`s (see the comment above the static items).
unsafe {
VALUE_NAME_SEGMENT_RE.assume_init_drop();
VALUE_NAME_RE.assume_init_drop();
POSSIBLE_VALUES_RE.assume_init_drop();
}
if n_errors == 0 {
ExitCode::SUCCESS
} else {
eprintln!("{n_errors} error(s) occurred.");
ExitCode::FAILURE
}
}
fn check_options(
options: &mut HashMap<&str, &Arg>,
man_page: &mut BufReader<File>,
) -> UnitResult<Vec<(i32, Error)>> {
let option_header_re = Regex::new(concat!(
r"^(?:``-(?<short_name>[a-zA-Z?])``, )?",
r"``--(?<long_name>[a-z]+(?:-[a-z]+)*)``",
r"(?: (?<value_names>\S.*?))?\s*$",
))
.unwrap();
let mut errors: Vec<(i32, Error)> = Vec::new();
let mut man_page_lines = man_page.lines().map(io::Result::unwrap).zip(1..).peekable();
for (line, _) in &mut man_page_lines {
if line == ".. START OF OPTIONS" {
break;
}
}
while let Some((line, line_no)) = man_page_lines.next() {
if line == ".. END OF OPTIONS" {
break;
}
if !line.starts_with("``-") {
continue;
}
if !option_header_re.is_match(&line) {
errors.push((
line_no,
InvalidFormat {
message: "Invalid option header format".to_string(),
string: line,
causes: &[
"no long name",
"no double backquotes around the short and/or long name",
"wrong amount of '-' before short and/or long name",
"no comma followed by space between the short and long name",
"multiple space between the short name and long name",
"the short name contains more than one character",
"the short name contains a character other than 'a'..'z', 'A'..'Z'",
"the long name contains a character other than 'a'..'z', '-'",
"no space between the long name and value name",
"multiple space between the long name and value name",
"value name after short name",
],
},
));
continue;
}
let header = option_header_re.captures(&line).unwrap();
let long_name = header.name("long_name").unwrap().as_str();
// Removing so we can easily know what options are undocumented at end.
if let Some(option) = options.remove(long_name) {
if let Err(option_errs) = check_option(
option,
long_name,
header
.name("short_name")
.map(|r#match| r#match.as_str().chars().next().unwrap()),
header.name("value_names").map(|r#match| r#match.as_str()),
&mut man_page_lines,
) {
errors.extend(option_errs.into_iter().map(|err| (line_no, err)));
}
} else {
errors.push((line_no, UnspecifiedOption(long_name.to_string())));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn check_option(
option: &Arg,
long_name: &str,
short_name: Option<char>,
value_names: Option<&str>,
man_page_lines: &mut Peekable<impl Iterator<Item = (String, i32)>>,
) -> UnitResult<Vec<Error>> {
let mut errors: Vec<Error> = Vec::new();
let value_range = match option.get_num_args() {
Some(value_range) => value_range,
None => match option.get_value_names() {
Some(value_names) => ValueRange::new(value_names.len()),
None => match option.get_action() {
ArgAction::Set | ArgAction::Append => ValueRange::SINGLE,
_ => ValueRange::EMPTY,
},
},
};
if let Err(error) = check_short_name(option, long_name, short_name) {
errors.push(*error);
}
if let Err(error) = check_value_names(option, long_name, &value_range, value_names) {
errors.push(*error);
}
if let Err(error) = check_description(option, long_name, &value_range, man_page_lines) {
errors.push(*error);
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn check_short_name(
option: &Arg,
long_name: &str,
short_name: Option<char>,
) -> UnitResult<Box<Error>> {
if let Some(documented) = short_name {
if let Some(specified) = option.get_short() {
if documented != specified {
return Err(Box::new(MismacthedShortNames {
option: long_name.to_string(),
documented,
specified,
}));
}
} else {
return Err(Box::new(UnspecifiedShortName {
option: long_name.to_string(),
short: documented,
}));
}
} else if let Some(short) = option.get_short() {
return Err(Box::new(UndocumentedShortName {
option: long_name.to_string(),
short,
}));
}
Ok(())
}
// NOTE: Even though this function accepts input for any kind/combination of option
// values, it currently doesn't handle multiple or optional values since `rsvg-convert`
// doesn't use any of such yet. In any such case, this function panics.
//
// If at some point in the future the basis for this is no longer valid, this function
// will need to be refactored.
fn check_value_names(
option: &Arg,
long_name: &str,
value_range: &ValueRange,
value_names_segment: Option<&str>,
) -> UnitResult<Box<Error>> {
assert!(
value_range.max_values() <= 1,
"Multiple option values are not yet handled: `--{}` takes a maximum of {} values",
long_name,
value_range.max_values(),
);
assert!(
value_range.min_values() == value_range.max_values(),
"Optional option values are not yet handled: `--{}` takes {} optional value(s)",
long_name,
value_range.max_values() - value_range.min_values(),
);
let value_name = if value_range.takes_values() {
Some(
option
.get_value_names()
.map_or(option.get_id().as_str(), |value_names| {
value_names[0].as_str()
}),
)
} else {
None
};
if let Some(value_names_segment_str) = value_names_segment {
let documented = get_value_name(long_name, value_names_segment_str)?;
if let Some(specified) = value_name {
if documented != specified {
return Err(Box::new(MismacthedValueNames {
option: long_name.to_string(),
documented: documented.to_string(),
specified: specified.to_string(),
}));
}
let value_name_re = unsafe { VALUE_NAME_RE.assume_init_ref() };
if !value_name_re.is_match(documented) {
return Err(Box::new(InvalidValueName {
option: long_name.to_string(),
value_name: documented.to_string(),
}));
}
} else {
return Err(Box::new(UnspecifiedValueName {
option: long_name.to_string(),
value_name: documented.to_string(),
}));
}
} else if let Some(specified) = value_name {
return Err(Box::new(UndocumentedValueName {
option: long_name.to_string(),
value_name: specified.to_string(),
}));
}
Ok(())
}
fn check_description(
option: &Arg,
long_name: &str,
value_range: &ValueRange,
man_page_lines: &mut Peekable<impl Iterator<Item = (String, i32)>>,
) -> UnitResult<Box<Error>> {
let description_lines = get_description_lines(long_name, man_page_lines)?;
check_description_indentation(long_name, &description_lines)?;
if value_range.takes_values() {
// If more description segments get checked at some point, the
// following statement should be moved outside this block.
let description_segments = get_description_segments(&description_lines);
check_value_description(option, long_name, &description_segments)?;
}
Ok(())
}
fn check_description_indentation(
long_name: &str,
description_lines: &[(String, i32)],
) -> UnitResult<Box<Error>> {
// First line's indentation.
let expected: String = description_lines[0]
.0
.chars()
.take_while(char::is_ascii_whitespace)
.collect();
for (line, line_no) in &description_lines[1..] {
if line
.strip_prefix(&expected)
// Less, or more indentation -> invalid.
.map_or(true, |line_indent_stripped| {
line_indent_stripped
.chars()
.next()
.unwrap()
.is_ascii_whitespace()
})
{
return Err(Box::new(InvalidDescriptionIndentation {
option: long_name.to_string(),
line_no: *line_no,
indent: line.chars().take_while(char::is_ascii_whitespace).collect(),
expected,
}));
}
}
Ok(())
}
fn check_value_description(
option: &Arg,
long_name: &str,
description_segments: &[(String, RangeInclusive<i32>)],
) -> UnitResult<Box<Error>> {
let possible_values = option.get_possible_values();
// Value description is only required for options with a fixed set of possible values.
if possible_values.is_empty() {
return Ok(());
}
if let Some((value_description, value_desc_range)) = description_segments.get(1) {
check_possible_values(
long_name,
&possible_values,
value_description,
value_desc_range,
)?;
} else {
return Err(Box::new(NoValueDescription {
option: long_name.to_string(),
required_because: "the option has a fixed set of possible values",
}));
}
Ok(())
}
fn check_possible_values(
long_name: &str,
possible_values: &[PossibleValue],
value_description: &str,
value_desc_range: &RangeInclusive<i32>,
) -> UnitResult<Box<Error>> {
let documented = get_possible_values(long_name, value_description, value_desc_range)?;
let specified: HashSet<&str> = possible_values
.iter()
.map(PossibleValue::get_name)
.collect();
if documented != specified {
return Err(Box::new(MismatchedPossibleValues {
option: long_name.to_string(),
line_range: value_desc_range.clone(),
documented: documented.into_iter().map(str::to_string).collect(),
specified: specified.into_iter().map(str::to_string).collect(),
}));
}
let invalid_values: Vec<&str> = documented
.into_iter()
.filter(|value| value.contains(|ch: char| ch.is_ascii_whitespace()))
.collect();
if !invalid_values.is_empty() {
return Err(Box::new(InvalidPossibleValues {
option: long_name.to_string(),
line_range: value_desc_range.clone(),
values: invalid_values.into_iter().map(str::to_string).collect(),
}));
}
Ok(())
}
fn get_description_lines(
long_name: &str,
man_page_lines: &mut Peekable<impl Iterator<Item = (String, i32)>>,
) -> Result<Vec<(String, i32)>, Box<Error>> {
let mut lines: Vec<(String, i32)> = Vec::new();
while let Some((line, _)) = man_page_lines.peek() {
// Skip blank/all-whitespace lines.
if line.chars().all(|ch| ch.is_ascii_whitespace()) {
man_page_lines.next();
continue;
}
// Description lines must be indented underneath the option header.
// The first line after the header not fulfiling this criteria marks
// the end of the description.
if !line.chars().next().unwrap().is_ascii_whitespace() {
break;
}
lines.push(man_page_lines.next().unwrap());
}
if lines.is_empty() {
Err(Box::new(NoDescription {
option: long_name.to_string(),
}))
} else {
Ok(lines)
}
}
fn get_description_segments(
description_lines: &[(String, i32)],
) -> Vec<(String, RangeInclusive<i32>)> {
let mut description_lines_iter = description_lines.iter().peekable();
let mut segments: Vec<(String, RangeInclusive<i32>)> = Vec::new();
while let Some(&&(_, segment_start)) = description_lines_iter.peek() {
let mut segment: Vec<&str> = Vec::new();
// Initialized with the number of the last line in case it doesn't end with '.'.
let mut segment_end = description_lines.last().unwrap().1;
for &(ref line, line_no) in &mut description_lines_iter {
let line_trimmed = line.trim();
segment.push(line_trimmed);
if line_trimmed.ends_with(".") {
segment_end = line_no;
break;
}
}
segments.push((segment.join(" "), segment_start..=segment_end));
}
segments
}
fn get_possible_values<'a>(
long_name: &str,
value_description: &'a str,
value_desc_range: &RangeInclusive<i32>,
) -> Result<HashSet<&'a str>, Box<Error>> {
let possible_values_re = unsafe { POSSIBLE_VALUES_RE.assume_init_ref() };
if let Some(possible_values) = possible_values_re.captures(value_description) {
Ok(possible_values
.get(1)
.unwrap()
.as_str()
.split(", ")
.map(|value| {
value
.trim()
.strip_prefix("``")
.unwrap()
.strip_suffix("``")
.unwrap()
})
.collect())
} else {
Err(Box::new(InvalidFormat {
message: format!(
"Invalid possible values description (lines {:?}) for option `--{}`",
value_desc_range, long_name,
),
string: value_description.to_string(),
causes: &[
"doesn't start on a new line",
r#"doesn't start with "Possible values are ""#,
"no double backquotes around values",
"no comma followed by space between values",
"no period '.' at the end of the description",
],
}))
}
}
fn get_value_name<'a>(long_name: &str, value_name_segment: &'a str) -> Result<&'a str, Box<Error>> {
let value_name_segment_re = unsafe { VALUE_NAME_SEGMENT_RE.assume_init_ref() };
if let Some(captures) = value_name_segment_re.captures(value_name_segment) {
Ok(captures.get(1).unwrap().as_str())
} else {
Err(Box::new(InvalidFormat {
message: format!("Invalid value name segment for option `--{}`", long_name),
string: value_name_segment.to_string(),
causes: &["the value name is not sorrounded by asterisks '*'"],
}))
}
}

60
ci/check_rust_versions.py Normal file
View File

@@ -0,0 +1,60 @@
# This script checks that the Minimum Supported Rust Version (MSRV) has the same value
# in several places throughout the source tree.
import re
import sys
PLACES_WITH_RUST_VERSION = [
['meson.build', r"msrv = '(.*)'"],
['Cargo.toml', r'rust-version\s*=\s*"(.*)"'],
['ci/container_builds.yml', r'RUST_MINIMUM:\s*"(.*)"'],
['devel-docs/_build_dependencies.rst', r'`rust .*`_ (.*) or later'],
]
PLACES_WITH_CARGO_CBUILD_VERSION = [
['meson.build', r"cargo_cbuild_version = '(.*)'"],
['librsvg-c/Cargo.toml', r'min_version = "(.*)"'],
]
def check_versions(name, places):
versions = []
for filename, regex in places:
r = re.compile(regex)
with open(filename) as f:
matched = False
for idx, line in enumerate(f.readlines()):
matches = r.search(line)
if matches is not None:
matched = True
line_number = idx + 1
versions.append([filename, line_number, matches.group(1), line])
if not matched:
raise Exception(f'file {filename} does not have a line that matches {regex}')
assert len(versions) > 0
all_the_same = True
for filename, line_number, version, line in versions[1:]:
if version != versions[0][2]:
all_the_same = False
if not all_the_same:
print(f'{name}: Version numbers do not match in these lines, please fix them!\n', file=sys.stderr)
for filename, line_number, version, line in versions:
print(f' {filename}:{line_number}: {line}', file=sys.stderr)
sys.exit(1)
print(f'{name}: Versions number match. All good!', file=sys.stderr)
def main():
check_versions('rustc', PLACES_WITH_RUST_VERSION)
check_versions('cargo-cbuild', PLACES_WITH_CARGO_CBUILD_VERSION)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -e
shellcheck --external-sources ci/*.sh

156
ci/container_builds.yml Normal file
View File

@@ -0,0 +1,156 @@
# The following includes are for the CI templates that are used as a
# base to construct our container images. They need to be updated periodically, but
# not frequently, by pointing them to a more recent commit.
include:
- remote: "https://gitlab.gnome.org/Infrastructure/freedesktop-ci-templates/-/raw/a4eb2bbf65c482f024cae7ee178b8fe0cfef0537/templates/opensuse.yml"
- remote: "https://gitlab.freedesktop.org/alatiera/ci-templates/-/raw/104fbc7115a99a450ba926d2a96208457d77cac0/templates/gnomeos.yml"
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
variables:
BASE_TAG: "2026-03-27.2-main"
RUST_STABLE: "1.92.0"
RUST_MINIMUM: "1.92.0"
RUST_NIGHTLY: "nightly"
RUSTUP_VERSION: "1.28.0"
GNOMEOS_STABLE: "core-50"
# This bunch of packages are the system's C/C++ compilers, and the indirect dependencies needed
# to build librsvg's direct dependencies. E.g. we must build cairo from a git tag, but we don't
# care about libpng too much and so use it as a system library.
.container.opensuse@common:
stage: "container-build"
before_script:
- source ./ci/env.sh
variables:
FDO_DISTRIBUTION_VERSION: "tumbleweed"
FDO_UPSTREAM_REPO: "gnome/librsvg"
FDO_DISTRIBUTION_PACKAGES: >-
autoconf
automake
bison
clang
clang-tools
curl
dav1d-devel
diffutils
findutils
flex
gawk
gcc
gcc-c++
gdb
gettext
gettext-tools
git
gobject-introspection-devel
google-roboto-fonts
gperf
itstool
libbrotli-devel
libbz2-devel
libexpat-devel
libffi-devel
libjson-c-devel
libpng-devel
libstdc++-devel
libtool
libuuid-devel
make
meson
openssl-devel
pcre2-devel
python3-pip
python3-devel
shadow
shared-mime-info
ShellCheck
system-group-wheel
vala
wget
xz
zlib-devel
.container.opensuse@x86_64.stable:
extends: .container.opensuse@common
variables:
FDO_DISTRIBUTION_TAG: "x86_64-${RUST_STABLE}-${BASE_TAG}"
FDO_DISTRIBUTION_EXEC: >-
bash ci/install-python-tools.sh &&
bash ci/install-rust.sh --rustup-version ${RUSTUP_VERSION} \
--stable ${RUST_STABLE} \
--minimum ${RUST_MINIMUM} \
--nightly ${RUST_NIGHTLY} \
--arch x86_64-unknown-linux-gnu &&
bash ci/install-cargo-cbuild.sh &&
bash ci/install-rust-linters.sh &&
bash ci/install-grcov.sh &&
mkdir -p /usr/local/librsvg &&
bash ci/build-dependencies.sh --prefix /usr/local/librsvg --meson-flags "--buildtype=release" &&
rm -rf /root/.cargo /root/.cache # cleanup compilation dirs; binaries are installed now
.container.opensuse@aarch64:
extends: .container.opensuse@common
variables:
FDO_DISTRIBUTION_TAG: "aarch64-${RUST_STABLE}-${BASE_TAG}"
FDO_DISTRIBUTION_EXEC: >-
bash ci/install-rust.sh --rustup-version ${RUSTUP_VERSION} \
--stable ${RUST_STABLE} \
--arch aarch64-unknown-linux-gnu &&
mkdir -p /usr/local/librsvg &&
bash ci/build-dependencies.sh --prefix /usr/local/librsvg --meson-flags "--buildtype=release" &&
rm -rf /root/.cargo /root/.cache # cleanup compilation dirs; binaries are installed now
tags:
- aarch64
opensuse-container@x86_64.stable:
extends:
- .fdo.container-build@opensuse@x86_64
- .container.opensuse@x86_64.stable
stage: "container-build"
opensuse-container@aarch64:
extends:
- .fdo.container-build@opensuse@aarch64
- .container.opensuse@aarch64
stage: "container-build"
.container.gnomeos@common:
stage: "container-build"
before_script:
- cat /etc/os-release
- source ./ci/env.sh
variables:
RUST_VERSION: "${RUST_STABLE}"
FDO_UPSTREAM_REPO: "gnome/librsvg"
FDO_DISTRIBUTION_EXEC: >-
bash ci/install-python-tools.sh &&
bash ci/install-rust.sh --rustup-version ${RUSTUP_VERSION} \
--stable ${RUST_STABLE} \
--arch x86_64-unknown-linux-gnu &&
bash ci/install-cargo-cbuild.sh &&
rm -rf /root/.cargo /root/.cache # cleanup compilation dirs; binaries are installed now
.container.gnomeos.nightly@x86_64:
extends: .container.gnomeos@common
variables:
FDO_DISTRIBUTION_TAG: "x86_64-${RUST_STABLE}-${BASE_TAG}"
FDO_DISTRIBUTION_VERSION: "core-nightly"
.container.gnomeos.stable@x86_64:
extends: .container.gnomeos@common
variables:
FDO_DISTRIBUTION_TAG: "x86_64-${RUST_STABLE}-${BASE_TAG}"
FDO_DISTRIBUTION_VERSION: "$GNOMEOS_STABLE"
gnomeos-container.nightly@x86_64:
extends:
- .fdo.container-build@gnomeos@x86_64
- .container.gnomeos.nightly@x86_64
stage: "container-build"
gnomeos-container.stable@x86_64:
extends:
- .fdo.container-build@gnomeos@x86_64
- .container.gnomeos.stable@x86_64
stage: "container-build"

21
ci/env.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
#
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
# Activate the Python virtual environment for CI scripts.
#
# We test for the presence of the file, since when first creating the container images for CI,
# the venv has not been created yet. This is mostly a hack to allow having a single "env.sh"
# script instead of one for container creation and one for CI jobs.
if [ -f /usr/local/python/bin/activate ]; then
source /usr/local/python/bin/activate
fi
export RUSTUP_HOME='/usr/local/rustup'
export PATH=$PATH:/usr/local/cargo/bin
if [ ! -v CARGO_HOME ]; then
export CARGO_HOME=/srv/project/cargo_cache
mkdir -p /srv/project/cargo_cache
fi

56
ci/gen-coverage.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/bin/sh
set -eu
mkdir -p public
call_grcov() {
output_type=$1
output_path=$2
# Explanation of the options below:
# grcov coverage-profiles _build - paths where to find .rawprof (llvm) and .gcda (gcc) files, respectively
# --binary-path ./_build/target/debug/ - where the Rust test binaries are located
# --source-dir . - toplevel source directory
# --branch - compute branch coverage if possible
# --ignore '**/build/markup5ever*' - ignore generated code from dependencies
# --ignore '**/build/cssparser*' - ignore generated code from dependencies
# --ignore 'cargo_cache/*' - ignore code from dependencies
# --ignore '_build/*' - ignore generated code
# --ignore 'rsvg-bench/*' - ignore benchmarks; they are not useful for the test coverage report
# --excl-line 'unreachable!' - ignore lines with the unreachable!() macro
# --output-type $output_type
# --output-path $output_path
grcov coverage-profiles \
--binary-path ./target/debug/ \
--source-dir . \
--branch \
--ignore 'cargo_cache/*' \
--ignore 'target/*' \
--excl-line 'unreachable!|panic!' \
--output-type "$output_type" \
--output-path "$output_path"
}
call_grcov html public/coverage
# Generate the cobertura XML format for GitLab's line-by-line coverage report in MR diffs.
#
# However, guard it for not being over 10 MB in size; we had a case before where it was over
# 500 MB and that OOM'd gitlab's redis.
call_grcov cobertura coverage.xml
size=$(wc -c < coverage.xml)
if [ "$size" -ge 10485760 ]
then
rm coverage.xml
echo "coverage.xml is over 10 MB, removing it so it will not be used"
fi
# Print "Coverage: 42.42" so .gitlab-ci.yml will pick it up with a regex
#
# We scrape this from the HTML report, not the JSON summary, because coverage.json
# uses no decimal places, just something like "42%".
grep -Eo 'abbr title.* %' public/coverage/index.html | head -n 1 | grep -Eo '[0-9.]+ %' | grep -Eo '[0-9.]+' | awk '{ print "Coverage:", $1 }'

7
ci/gen-devel-docs.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
mkdir -p public/devel-docs
sphinx-build --fail-on-warning --keep-going devel-docs public/devel-docs

20
ci/gen-rust-docs.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
#
# Generates the Rust documentation in the following directories:
# public/internals - internals documentation, for librsvg development
# public/doc - public API documentation
set -eu
# turn warnings into errors
export RUSTDOCFLAGS='-D warnings'
cargo doc --workspace --document-private-items --no-deps
# cargo doc --document-private-items --no-deps --package librsvg
mkdir -p public/internals
mv target/doc/* public/internals
cargo doc --no-deps --package librsvg --package 'librsvg-rebind*'
# cargo doc --no-deps --package librsvg
mkdir -p public/doc
cp -r target/doc/* public/doc

View File

@@ -0,0 +1,11 @@
#!/bin/bash
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
source ./ci/env.sh
set -eu
export CARGO_HOME='/usr/local/cargo'
cargo install --force --locked cargo-c --version 0.10.10

13
ci/install-grcov.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
#
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
source ./ci/env.sh
set -eu
export CARGO_HOME='/usr/local/cargo'
# Coverage tools
cargo install grcov
rustup component add llvm-tools-preview

View File

@@ -0,0 +1,15 @@
#!/bin/bash
#
# Creates a Python virtual environment in /usr/local/python and installs
# the modules from requirements.txt in it. These modules are required
# by various jobs in the CI pipeline.
#
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
set -eux -o pipefail
python3 -m venv /usr/local/python
source /usr/local/python/bin/activate
pip3 install --upgrade pip
pip3 install -r ci/requirements.txt

View File

@@ -0,0 +1,16 @@
#!/bin/bash
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
source ./ci/env.sh
set -eu
export CARGO_HOME='/usr/local/cargo'
rustup component add clippy
rustup component add rustfmt
cargo install --version ^1.0 gitlab_clippy
cargo install --force --locked cargo-deny
cargo install --force --locked rumdl --version 0.1.11
# cargo install --force cargo-outdated

90
ci/install-rust.sh Executable file
View File

@@ -0,0 +1,90 @@
#!/bin/bash
#
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
set -o errexit -o pipefail -o noclobber -o nounset
source ./ci/env.sh
export CARGO_HOME='/usr/local/cargo'
PARSED=$(getopt --options '' --longoptions 'rustup-version:,stable:,minimum:,nightly:,arch:' --name "$0" -- "$@")
eval set -- "$PARSED"
unset PARSED
RUSTUP_VERSION=
STABLE=
MINIMUM=
NIGHTLY=
ARCH=
while true; do
case "$1" in
'--rustup-version')
RUSTUP_VERSION=$2
shift 2
;;
'--stable')
STABLE=$2
shift 2
;;
'--minimum')
MINIMUM=$2
shift 2
;;
'--nightly')
NIGHTLY=$2
shift 2
;;
'--arch')
ARCH=$2
shift 2
;;
'--')
shift
break
;;
*)
echo "Programming error"
exit 3
;;
esac
done
if [ -z "$RUSTUP_VERSION" ]; then
echo "missing --rustup-version argument"
exit 1
fi
if [ -z "$STABLE" ]; then
echo "missing --stable argument, please pass the stable version of rustc you want"
exit 1
fi
if [ -z "$ARCH" ]; then
echo "missing --arch argument, please pass an architecture triple like x86_64-unknown-linux-gnu"
exit 1
fi
RUSTUP_URL="https://static.rust-lang.org/rustup/archive/$RUSTUP_VERSION/$ARCH/rustup-init"
wget "$RUSTUP_URL"
chmod +x rustup-init
./rustup-init -y --no-modify-path --profile minimal --default-toolchain "$STABLE"
rm rustup-init
chmod -R a+w "$RUSTUP_HOME" "$CARGO_HOME"
if [ -n "$MINIMUM" ]; then
rustup toolchain install "$MINIMUM"
fi
if [ -n "$NIGHTLY" ]; then
rustup toolchain install "$NIGHTLY"
fi

11
ci/msvc-requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
# This file is used from ci/test-msvc.bat. It lists the Python packages required for
# building librsvg and its dependencies on the CI.
meson==1.7.2
jinja2
markdown
markupsafe
packaging
pygments
typogrify
tomli

17
ci/pages-index.html Normal file
View File

@@ -0,0 +1,17 @@
<html>
<head>
<title>Librsvg's cabinet of curiosities</title>
</head>
<body>
<h1>Librsvg's cabinet of curiosities</h1>
<ul>
<li><a href="Rsvg-2.0/">C API documentation</a></li>
<li><a href="doc/rsvg/">Rust API documentation</a></li>
<li><a href="coverage/">Test coverage report</a></li>
<li><a href="devel-docs/">Development guide for librsvg</a></li>
<li><a href="internals/rsvg/">Library internals documentation</a></li>
</ul>
</body>
</html>

21
ci/pkgconfig.nmake.patch Normal file
View File

@@ -0,0 +1,21 @@
--- a/Makefile.vc 2023-09-16 11:28:40.214382500 +0800
+++ b/Makefile.vc 2023-09-18 10:37:02.810835600 +0800
@@ -73,7 +73,7 @@
@-if exist $@.manifest mt /manifest $@.manifest /outputresource:$@;1
$(CFG)\$(PLAT)\pkg-config:
- @-mkdir $@
+ @-md $@
config.h: config.h.win32
@-copy $@.win32 $@
@@ -84,7 +84,8 @@
@-del /f /q $(CFG)\$(PLAT)\*.exe
@-del /f /q $(CFG)\$(PLAT)\*.ilk
@-del /f /q $(CFG)\$(PLAT)\pkg-config\*.obj
- @-rmdir /s /q $(CFG)\$(PLAT)
+ @-del /f /q $(CFG)\$(PLAT)\pkg-config\*.pdb
+ @-rd $(CFG)\$(PLAT)
@-del vc$(VSVER)0.pdb
@-del config.h

View File

@@ -0,0 +1,34 @@
#!/bin/bash
#
# Utility script so you can pull the container image from CI for local development.
# Run this script and follow the instructions; the script will tell you how
# to run "podman run" to launch a container that has the same environment as the
# one used during CI pipelines. You can debug things at leisure there.
set -eu
set -o pipefail
CONTAINER_BUILDS=ci/container_builds.yml
if [ ! -f $CONTAINER_BUILDS ]
then
echo "Please run this from the toplevel source directory in librsvg"
exit 1
fi
tag=$(grep -e '^ BASE_TAG:' $CONTAINER_BUILDS | head -n 1 | sed -E 's/.*BASE_TAG: "(.+)"/\1/')
rust_version=$(grep -e '^ RUST_STABLE:' $CONTAINER_BUILDS | head -n 1 | sed -E 's/.*RUST_STABLE: "(.+)"/\1/')
full_tag=x86_64-$rust_version-$tag
image_name=registry.gitlab.gnome.org/gnome/librsvg/opensuse/tumbleweed:$full_tag
echo pulling image "$image_name"
podman pull "$image_name"
echo ""
echo "You can now run this:"
echo " podman run --rm -ti --cap-add=SYS_PTRACE -v \$(pwd):/srv/project:z -w /srv/project $image_name"
echo ""
echo "Don't forget to run this once inside the container:"
echo " source ci/env.sh"
echo " source ci/setup-dependencies-env.sh"

7
ci/pyproject.toml Normal file
View File

@@ -0,0 +1,7 @@
# Configuration for the Ruff linter for Python.
#
# Repository: https://github.com/astral-sh/ruff
#
# Documentation: https://beta.ruff.rs/docs/
[tool.ruff]
line-length = 100 # same as rustfmt

13
ci/requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
docutils
furo
gi-docgen
pytest
ruff
semver
setuptools
Sphinx
sphinx-issues
toml

View File

@@ -0,0 +1,17 @@
#!/bin/bash
#
# IMPORTANT: See
# https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/ci.html#container-image-version
if [ -z "$PREFIX" ]; then
echo "Using default prefix /usr/local/librsvg for dependencies."
echo "If this is not what you want, set the PREFIX variable"
echo "before sourcing this script."
PREFIX=/usr/local/librsvg
fi
export PATH=$PREFIX/bin:$PATH
export LD_LIBRARY_PATH=$PREFIX/lib64
export PKG_CONFIG_PATH=$PREFIX/lib64/pkgconfig
export XDG_DATA_DIRS=${PREFIX}/share:/usr/share
export ACLOCAL_PATH=${PREFIX}/share/aclocal

126
ci/test-msvc.bat Normal file
View File

@@ -0,0 +1,126 @@
@echo on
:: vcvarsall.bat sets various env vars like PATH, INCLUDE, LIB, LIBPATH for the
:: specified build architecture
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64
@echo on
:: set PATH, LIB and INCLUDE to first include our install directory, as well as to where
:: `tar`, `bzip2` and `gzip` are.
@set INST=%CD%\rsvg.ci.bin
@set INST_PSX=%INST:\=/%
@set MSYS2_BINDIR=c:\msys64\usr\bin
@set BASEPATH=%INST%\bin;%PATH%
@set PATH=%BASEPATH%
@set LIB=%INST%\lib;%LIB%
@set INCLUDE=%INST%\include\glib-2.0;%INST%\lib\glib-2.0\include;%INST%\include;%INCLUDE%
@set RUST_HOST=x86_64-pc-windows-msvc
:: Packaged dep versions
@set LIBXML2_VER=2.12.6
@set FREETYPE2_VER=2.13.0
@set PKG_CONFIG_VER=0.29.2
@set CURRDIR=%CD%
pip3 install --upgrade --user -r ci/msvc-requirements.txt || goto :error
git clone --depth 1 --no-tags https://gitlab.gnome.org/GNOME/gdk-pixbuf.git
git clone --depth 1 --no-tags https://gitlab.gnome.org/GNOME/pango.git
:: build and install GDK-Pixbuf (includes glib, libpng, libjpeg-turbo and their deps)
md _build_gdk_pixbuf
cd _build_gdk_pixbuf
meson setup ../gdk-pixbuf --buildtype=release --prefix=%INST_PSX% -Dman=false -Ddocumentation=false
ninja install || goto :error
cd ..
rmdir /s/q _build_gdk_pixbuf
copy /b %INST%\lib\z.lib %INST%\lib\zlib.lib
:: Download rustup-init, pkg-config, FreeType and libxml2
:: (sadly there is no CUrl, but wget, so MSYS2 is needed temporarily)
:: %MSYS2_BINDIR% must be in PATH to find gzip/xz.
set PATH=%PATH%;%MSYS2_BINDIR%
if not exist %HOMEPATH%\.cargo\bin\rustup.exe %MSYS2_BINDIR%\wget https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe
%MSYS2_BINDIR%\wget https://pkgconfig.freedesktop.org/releases/pkg-config-%PKG_CONFIG_VER%.tar.gz
%MSYS2_BINDIR%\wget https://downloads.sourceforge.net/freetype/freetype-%FREETYPE2_VER%.tar.xz
%MSYS2_BINDIR%\wget https://download.gnome.org/sources/libxml2/2.12/libxml2-%LIBXML2_VER%.tar.xz
:: Ensure it sets the filename correctly
%MSYS2_BINDIR%\wget --content-disposition https://wrapdb.mesonbuild.com/v2/libxml2_%LIBXML2_VER%-1/get_patch
%MSYS2_BINDIR%\tar -xf pkg-config-%PKG_CONFIG_VER%.tar.gz
%MSYS2_BINDIR%\tar -Jxf freetype-%FREETYPE2_VER%.tar.xz
%MSYS2_BINDIR%\tar -Jxf libxml2-%LIBXML2_VER%.tar.xz
:: Sorry for this hack! Please remove when the runner gets unzip through Pacman
python -c "import zipfile; zipfile.ZipFile('libxml2_%LIBXML2_VER%-1_patch.zip', 'r').extractall()"
:: Having the gnutools/msys64 in the %PATH% during the MSVC builds
:: can cause trouble...
del /f/q pkg-config-%PKG_CONFIG_VER%.tar.gz freetype-%FREETYPE2_VER%.tar.xz libxml2-%LIBXML2_VER%.tar.xz libxml2_%LIBXML2_VER%-1_patch.zip
:: build and install pkg-config
cd pkg-config-%PKG_CONFIG_VER%
:: patch pkg-config's NMake Makefile so that GNU's mkdir won'y be used by accident
%MSYS2_BINDIR%\patch -p1 < %CURRDIR:\=/%/ci/pkgconfig.nmake.patch
set PATH=%BASEPATH%
nmake /f Makefile.vc CFG=release || goto :error
copy /b release\x64\pkg-config.exe %INST%\bin
nmake /f Makefile.vc CFG=release clean
cd ..
:: build and install FreeType
md _build_ft
cd _build_ft
meson setup ../freetype-%FREETYPE2_VER% --buildtype=release --prefix=%INST_PSX% --pkg-config-path=%INST%\lib\pkgconfig --cmake-prefix-path=%INST%
ninja install || goto :error
cd ..
rmdir /s/q _build_ft
:: build and install libxml2 (use the Meson wrap overlaid before)
md _build_libxml
cd _build_libxml
meson setup ../libxml2-%LIBXML2_VER% --buildtype=release --prefix=%INST_PSX% -Diconv=disabled --pkg-config-path=%INST%\lib\pkgconfig --cmake-prefix-path=%INST%
ninja install || goto :error
cd ..
rmdir /s/q _build_libxml
:: build and install Pango (with HarfBuzz and Cairo)
md _build_pango
cd _build_pango
meson setup ../pango --buildtype=release --prefix=%INST_PSX% -Dfontconfig=disabled --pkg-config-path=%INST%\lib\pkgconfig
ninja install || goto :error
cd ..
rmdir /s/q _build_pango
:: Install Rust
if exist %HOMEPATH%\.cargo\bin\rustup.exe %HOMEPATH%\.cargo\bin\rustup update
if not exist %HOMEPATH%\.cargo\bin\rustup.exe rustup-init -y --default-toolchain=stable-%RUST_HOST% --default-host=%RUST_HOST%
%HOMEPATH%\.cargo\bin\cargo install cargo-c || goto :error
:: Enable workaround if latest stable Rust caused issues like #968.
:: Update RUST_DOWNGRADE_VER below as well as required.
@set DOWNGRADE_RUST_VERSION=0
:: now build librsvg
set PATH=%PATH%;%HOMEPATH%\.cargo\bin
set PKG_CONFIG=%INST%\bin\pkg-config.exe
md msvc-build
cd msvc-build
:: Fix linking to PCRE for CI's sake
if exist %INST%\lib\libpcre2-8.a copy /b %INST%\lib\libpcre2-8.a %INST%\lib\pcre2-8.lib
if not "%DOWNGRADE_RUST_VERSION%" == "1" goto :normal_rust_build
@set RUST_DOWNGRADE_VER=1.82.0
%HOMEPATH%\.cargo\bin\rustup install %RUST_DOWNGRADE_VER%-%RUST_HOST%
meson setup .. --buildtype=release --prefix=%INST_PSX% --pkg-config-path=%INST%\lib\pkgconfig --cmake-prefix-path=%INST% -Dtriplet=%RUST_HOST% -Drustc-version=%RUST_DOWNGRADE_VER% || goto :error
goto :continue_build
:normal_rust_build
meson setup .. --buildtype=release --prefix=%INST_PSX% --pkg-config-path=%INST%\lib\pkgconfig --cmake-prefix-path=%INST% || goto :error
:continue_build
ninja || goto :error
ninja test
ninja install || goto :error
goto :EOF
:error
exit /b 1

48
ci/test-msys2.sh Normal file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
set -e
if [[ "$MSYSTEM" == "MINGW32" ]]; then
export MSYS2_ARCH="i686"
else
export MSYS2_ARCH="x86_64"
fi
pacman --noconfirm -Suy
pacman --noconfirm -S --needed \
base-devel \
pactoys
pacboy --noconfirm -S --needed \
meson:p \
cargo-c:p \
gi-docgen:p \
gobject-introspection:p \
gdk-pixbuf2:p \
harfbuzz:p \
fontconfig:p \
fribidi:p \
libthai:p \
cairo:p \
pango:p \
python-docutils:p \
libxml2:p \
toolchain:p \
rust:p \
cantarell-fonts:p
# https://github.com/rust-lang/cargo/issues/10885
CARGO=$(where cargo)
export CARGO
RUSTC=$(where rustc)
export RUSTC
meson setup _build -Dauto_features=disabled -Dpixbuf{,-loader}=enabled
meson compile -C _build
export RUST_BACKTRACE=1
TESTS_OUTPUT_DIR=$(pwd)/tests/output
export TESTS_OUTPUT_DIR
# meson test -C _build --print-errorlogs

12
ci/utils.py Normal file
View File

@@ -0,0 +1,12 @@
import re
def get_project_version_str():
regex = re.compile(r" +version: '(\d+\.\d+\.\d+)',")
with open("meson.build") as f:
for line in f.readlines():
matches = regex.match(line)
if matches is not None:
version_str = matches.group(1)
return version_str
raise Exception('meson.build does not have a version string for the project')