diff --git a/android_connection/apply_to_android.py b/android_connection/apply_to_android.py index 8eff732d43..55fc88766a 100644 --- a/android_connection/apply_to_android.py +++ b/android_connection/apply_to_android.py @@ -1,13 +1,30 @@ +from absl import app +from absl import flags +import filecmp +from lxml import etree from pathlib import Path -import sys +import shutil + + +FLAGS = flags.FLAGS + + +flags.DEFINE_string("android_root", None, "Root of android repo") +flags.DEFINE_bool("dry_run", True, "If False actually update files") def _require_exists(a_file: Path) -> Path: if not a_file.is_file(): - raise ValueError(f"{a_file} not found") + raise ValueError(f"{a_file} missing or not a file") return a_file +def _require_dir_exists(a_dir: Path) -> Path: + if not a_dir.is_dir(): + raise ValueError(f"{a_dir} missing or not a dir") + return a_dir + + def _fonts_xml(android_dir: Path) -> Path: return _require_exists(android_dir / "frameworks" / "base" / "data" / "fonts" / "fonts.xml") @@ -16,18 +33,106 @@ def _fonts_mk(android_dir: Path) -> Path: return _require_exists(android_dir / "external" / "noto-fonts" / "fonts.mk") +def _font_dir(android_dir: Path) -> Path: + return _require_dir_exists(android_dir / "external" / "noto-fonts" / "other") + + +def _repo_root() -> Path: + root = (Path(__file__).parent.parent).absolute() + if not (root / "LICENSE").is_file(): + raise IOError(f"{root} does not contain LICENSE") + return root + + +def noto_4_android_path() -> Path: + xml_file = _repo_root() / "android_connection" / "noto-fonts-4-android.xml" + if not xml_file.is_file(): + raise IOError(f"No file {xml_file}") + return xml_file + + +def font_file(font_el) -> str: + return ("".join(font_el.itertext())).strip() + + +def font_path(font_el) -> Path: + name = font_file(font_el) + path = font_el.attrib["path"] + return _require_exists(_repo_root() / path / name) + + def _validate_android_path(android_dir: Path): assert android_dir.is_dir(), f"{android_dir} should be a directory" - _fonts_xml(android_dir) # just to trigger it's checks - _fonts_mk(android_dir) # just to trigger it's checks + # just to trigger existance validation + _fonts_xml(android_dir) + _fonts_mk(android_dir) + _font_dir(android_dir) -def main(): - if len(sys.argv) != 2: - raise ValueError("Must have one arg, path to an Android checkout") - android_dir = Path(sys.argv[1]) +def main(_): + if not FLAGS.android_root: + raise ValueError("Must provide --android_root") + android_dir = Path(FLAGS.android_root) _validate_android_path(android_dir) + # gather fonts that should be copied to Android + noto_for_android = etree.parse(str(noto_4_android_path())) + new_paths = {} + for font_el in noto_for_android.xpath("//font[@path]"): + path = font_path(font_el) + if new_paths.get(path.name, path) != path: + raise IOError(f"Multiple paths for {path.name}") + new_paths[path.name] = path + old_paths = {p.name: p for p in _font_dir(android_dir).glob("Noto*.[ot]t[fc]")} + + new_names = set(new_paths.keys()) + old_names = set(old_paths.keys()) + + delta_sz = 0 + + deleted_files = old_names - new_names + print(f"{len(deleted_files)} DELETED") + for delete_me in sorted(deleted_files): + print(f" {delete_me}") + if not FLAGS.dry_run: + old_paths[delete_me].unlink() + del delete_me + del deleted_files + print() + + added_files = new_names - old_names + print(f"{len(added_files)} ADDED") + for add_me in sorted(added_files): + dest = _font_dir(android_dir) / add_me + print(f" {add_me}") + if not FLAGS.dry_run: + shutil.copy(new_paths[add_me], dest) + del add_me + del added_files + print() + + updated_files = new_names & old_names + untouched = 0 + print(f"{len(updated_files)} UPDATED") + for update_me in sorted(updated_files): + if filecmp.cmp(new_paths[update_me], old_paths[update_me], shallow=False): + untouched += 1 + continue + + dest = _font_dir(android_dir) / update_me + print(f" {update_me}") + if not FLAGS.dry_run: + shutil.copy(new_paths[update_me], dest) + del update_me + del updated_files + print() + + print(f"{untouched} did not change") + print() + + print(f"Done updating files, you should manually update {_fonts_xml(android_dir)}" + f" from {_noto_4_android()}") + if __name__ == "__main__": - main() \ No newline at end of file + app.run(main) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 982dcb7cd4..351a20fcfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +absl-py>=0.9.0 fonttools>=4.28.5 lxml>=4.7.1 pytest>=6.2.5 diff --git a/tests/apply_to_android_test.py b/tests/apply_to_android_test.py index f7d7d88aa5..1ba652d9cf 100644 --- a/tests/apply_to_android_test.py +++ b/tests/apply_to_android_test.py @@ -1,4 +1,5 @@ -from android_connection import apply_to_android +from android_connection.apply_to_android import _validate_android_path +from lxml import etree from pathlib import Path import pytest @@ -11,10 +12,11 @@ def _fake_android_dir() -> Path: return _testdata_dir() / "fake_android" -def test_apply_to_bad_dir(): - with pytest.raises(ValueError, match="not found"): - apply_to_android._validate_android_path(_testdata_dir()) +def test_validate_bad_dir(): + with pytest.raises(ValueError, match="missing"): + _validate_android_path(_testdata_dir()) -def test_apply_to_good_dir(): - apply_to_android._validate_android_path(_fake_android_dir()) +def test_validate_good_dir(): + _validate_android_path(_fake_android_dir()) + diff --git a/tests/noto_fonts_for_android_test.py b/tests/noto_fonts_for_android_test.py index 2a1bacd6af..6cb34a9dda 100644 --- a/tests/noto_fonts_for_android_test.py +++ b/tests/noto_fonts_for_android_test.py @@ -4,6 +4,11 @@ from pathlib import Path import pytest from typing import Tuple +from android_connection.apply_to_android import ( + font_file, + font_path, + noto_4_android_path, +) _KNOWN_PATHLESS = { @@ -14,37 +19,12 @@ } - -def _repo_root() -> Path: - root = (Path(__file__).parent / "..").absolute() - if not (root / "LICENSE").is_file(): - raise IOError(f"{root} does not contain LICENSE") - return root - - -def _noto_4_android_file() -> Path: - xml_file = _repo_root() / "android_connection" / "noto-fonts-4-android.xml" - if not xml_file.is_file(): - raise IOError(f"No file {xml_file}") - return xml_file - - -def _font_file(font_el) -> str: - return ("".join(font_el.itertext())).strip() - - -def _font_path(font_el) -> Path: - name = _font_file(font_el) - path = font_el.attrib["path"] - return _repo_root() / path / name - - def _is_collection(font_el) -> bool: - return _font_file(font_el).lower().endswith(".ttc") + return font_file(font_el).lower().endswith(".ttc") def _open_font(font_el) -> ttLib.TTFont: - path = _font_path(font_el) + path = font_path(font_el) if not path.is_file(): raise IOError(f"No such file: {path}") @@ -80,35 +60,35 @@ def _weight(font: ttLib.TTFont) -> Tuple[int, int, int]: def test_fonts_have_path(): - root = etree.parse(str(_noto_4_android_file())) + root = etree.parse(str(noto_4_android_path())) bad = [] for font in root.iter("font"): - font_file = _font_file(font) - if font_file in _KNOWN_PATHLESS: - assert "path" not in font.attrib, f"{font_file} not expected to have path. Correct _KNOWN_PATHLESS if you just added path" + name = font_file(font) + if name in _KNOWN_PATHLESS: + assert "path" not in font.attrib, f"{name} not expected to have path. Correct _KNOWN_PATHLESS if you just added path" continue if not font.attrib.get("path", ""): - bad.append(font_file) + bad.append(name) assert not bad, "Missing path attribute: " + ", ".join(bad) def test_ttcs_have_index(): - root = etree.parse(str(_noto_4_android_file())) + root = etree.parse(str(noto_4_android_path())) bad = [] for font in root.iter("font"): if not _is_collection(font): continue if "index" not in font.attrib: - bad.append(_font_file(font)) + bad.append(font_file(font)) assert not bad, "Missing index attribute: " + ", ".join(bad) def test_font_paths_are_valid(): - root = etree.parse(str(_noto_4_android_file())) + root = etree.parse(str(noto_4_android_path())) bad = [] for font in root.xpath("//font[@path]"): - path = _font_path(font) + path = font_path(font) if not path.is_file(): bad.append(str(path)) assert not bad, "No such file: " + ", ".join(bad) @@ -120,17 +100,17 @@ def test_font_weights(): "NotoNastaliqUrdu-Bold.ttf weight 700 outside font capability 400..400", "NotoSerifMyanmar-Bold.ttf weight 700 outside font capability 400..400" } - root = etree.parse(str(_noto_4_android_file())) + root = etree.parse(str(noto_4_android_path())) errors = [] for font_el in root.xpath("//font[@path]"): xml_weight = int(font_el.attrib["weight"]) - path = _font_path(font_el) + path = font_path(font_el) font = _open_font(font_el) min_wght, default_wght, max_weight = _weight(font) if xml_weight < min_wght or xml_weight > max_weight: - error_str = f"{_font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}" + error_str = f"{font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}" if error_str not in expected_errors: errors.append(error_str) @@ -138,21 +118,23 @@ def test_font_weights(): def test_font_full_weight_coverage(): - root = etree.parse(str(_noto_4_android_file())) + root = etree.parse(str(noto_4_android_path())) errors = [] for family in root.iter("family"): font_to_xml_weights = collections.defaultdict(set) for font in family.xpath("//font[@path]"): - font_to_xml_weights[(_font_path(font), font.attrib.get("index", -1))].add(int(font.attrib["weight"])) + path = font_path(font) + ttc_idx = font.attrib.get("index", -1) + font_to_xml_weights[(path, ttc_idx)].add(int(font.attrib["weight"])) # now you have a map of font path => set of weights in xml - for (font_path, font_number), xml_weights in font_to_xml_weights.items(): + for (path, ttc_idx), xml_weights in font_to_xml_weights.items(): # open the font, compute the 100 weights between it's min/max weight # if xml_weights != computed weights add this to the error list - font = _open_font_path(font_path, font_number) + font = _open_font_path(path, ttc_idx) min_wght, default_wght, max_weight = _weight(font) if min(xml_weights) > min_wght or max(xml_weights) < max_weight: - errors.append(f"{font_path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}") + errors.append(f"{path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}") assert not errors, ", ".join(errors) diff --git a/tests/testdata/fake_android/external/noto-fonts/other/NotoExisting.ttf b/tests/testdata/fake_android/external/noto-fonts/other/NotoExisting.ttf new file mode 100644 index 0000000000..e69de29bb2