. "$BST_LIB" # BST-TIMEOUT: 2400 # ONE python3 start, and on top of it TWO complete offline # # documentation builds. The start alone imports sphinx, # # docutils, jinja2 and pygments -- 99-python3-sphinx # # measured that shape and declared 1500 for one HTML build. # # This suite runs the `json' and `pickle' builders over one # # two-document source tree, sharing a doctree directory so # # the second reuses the first's parsed environment; the # # marginal cost of the second build is the write half only. # # Everything else in this file reads the filesystem and # # dpkg with shell builtins or greps the single probe run's # # output, which is nothing beside the interpreter start. # # 2400 is BST_SUITE_TIMEOUT_MAX, and it is asked for # # because the honest estimate is 1500 plus a write pass on # # a guest whose rate is not fast and not steady. # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed goes # # into the test plan rather than into this comment, which # # would cost an image build to keep the shipped suite # # byte-identical. # # python3-sphinxcontrib-serializinghtml 1.1.10 (poky/meta recipes-devtools/ # python/python3-sphinxcontrib-serializinghtml_1.1.10.bb, `inherit pypi # python_flit_core', with a meta-brintos bbappend that corrects RDEPENDS and # does nothing else) on the wasm32-hwjs glibc image -- THE SPHINX BUILDERS THAT # WRITE THE RENDERED DOCUMENTATION AS DATA RATHER THAN AS PAGES: `json' and # `pickle', each dumping the per-page context another program renders or # indexes. One of the five builder distributions split out of Sphinx core. # # ============================================================================ # THIS SUITE EXISTS BECAUSE A CYCLE CLOSED, AND IT IS THE FIRST TO SAY SO. # ============================================================================ # # sphinxcontrib/serializinghtml/__init__.py opens with FOUR unconditional # module-scope sphinx imports -- lines 9-12, sphinx.application, # sphinx.builders.html, sphinx.locale, sphinx.util.osutil -- none guarded by # TYPE_CHECKING and none deferrable, because SerializingHTMLBuilder subclasses # StandaloneHTMLBuilder and so needs Sphinx at CLASS-DEFINITION time. Until # python3-sphinx landed (BRINTOS-2192) this image had no Sphinx, and BOTH # # python3 -c 'import sphinxcontrib.serializinghtml' # python3 -c 'import sphinxcontrib.serializinghtml.jsonimpl' # # raised ModuleNotFoundError: No module named 'sphinx' -- the submodule too, # since importing it executes the package __init__ first. That is what parked # this ticket, and it is why jsonimpl being pure stdlib did not rescue it: the # serialiser worked and was unreachable by its own import path. # # The four sibling sphinxcontrib suites already on this image encode the other # side of that fact: each carries an `import-needs-sphinx' tripwire asserting # the ImportError, pre-registered to go RED "on the day python3-sphinx lands". # It landed, and they did. THIS suite is written from the other end -- it # asserts the import SUCCEEDS -- so a regression that removed Sphinx from the # image would turn this file red and those four green, which is the pair of # signals that says what happened rather than merely that something did. # # ============================================================================ # WHAT IS ACTUALLY ASSERTED, IN FOUR LAYERS. # ============================================================================ # # the tree -- the two modules, their .pyc files and the 54 message # catalogues the .deb installs, asserted as files, so a red # separates "not installed" from "installed and broken"; # the stanza -- the dpkg Depends line, where the bbappend's correction is # asserted as a DECLARATION, including the entry it # deliberately does NOT make; # the contract -- setup() against a stub application: the builders it # registers, the Sphinx version it requires, the extension it # pulls in, the catalogue it installs, and the dict it # returns. This is the whole of what a Sphinx extension is; # the builds -- two REAL documentation builds through the two builders, # with their serialised output read back and compared field # by field, and the two formats compared against each other. # # NO NETWORK, ANYWHERE. The probe writes its source tree into a temporary # directory and removes it; intersphinx is not enabled and nothing resolves a # URL. This port closes on this suite: non-interactive, no browser pass, no # BRINTOS-780 follow-on. # # WHY PURENESS IS LOAD-BEARING. This image's python3 is one static binary: # lib-dynload is empty by construction, this arm has no dynamic loader at all # (BRINTOS-1476), and the Py* export surface is empty (BRINTOS-2046). This # distribution is pure Python (`Root-Is-Purelib: true', `Tag: py3-none-any') # and a release that grew a compiled extension would install into site-packages # and die at the import. The no-.so cases assert that negative directly. P=python3-sphinxcontrib-serializinghtml require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages NS=$SITE/sphinxcontrib PKGDIR=$NS/serializinghtml DI=$SITE/sphinxcontrib_serializinghtml-1.1.10.dist-info PKGLIST=/var/lib/dpkg/info/python3-sphinxcontrib-serializinghtml.list PROBE=$BST_DATA/python3-sphinxcontrib-serializinghtml-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-serializinghtml.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --- the tree the package installs ------------------------------------------ check $P/site-packages-dir test -d "$SITE" check $P/namespace-dir test -d "$NS" check $P/package-dir test -d "$PKGDIR" check $P/src-init test -s "$PKGDIR/__init__.py" check $P/src-jsonimpl test -s "$PKGDIR/jsonimpl.py" check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-jsonimpl test -s "$PKGDIR/__pycache__/jsonimpl.cpython-312.pyc" # `sphinxcontrib' IS A PEP 420 IMPLICIT NAMESPACE PACKAGE -- the directory ships # NO __init__.py, and must not. Seven independently packaged distributions share # it on this image; a stray __init__.py from any one of them would shadow all the # others. The runtime half of the same assertion is `probe-namespace-no-file'. check_eq $P/namespace-no-init "" "$(ls "$NS/__init__.py" 2>/dev/null)" check $P/namespace-has-siblings test -d "$NS/applehelp" # --- the message catalogues -------------------------------------------------- # 54 languages, each a .mo the runtime loads and the .po it was compiled from, # plus the .pot template beside them. Counted through a pipeline rather than # named on a command line: 108 paths would be an ordinary-looking glob and an # argv the guest cannot exec (brintos/hardwarejs#15). check $P/locales-dir test -d "$PKGDIR/locales" check_eq $P/locales-mo-count "54" "$(find "$PKGDIR/locales" -name '*.mo' 2>/dev/null | wc -l | tr -d ' ')" check_eq $P/locales-po-count "54" "$(find "$PKGDIR/locales" -name '*.po' 2>/dev/null | wc -l | tr -d ' ')" check $P/locales-pot test -s "$PKGDIR/locales/sphinxcontrib.serializinghtml.pot" # Three spot checks, so a count that was right about a tree of empty files is # not the only thing standing here. check $P/locales-de-mo test -s "$PKGDIR/locales/de/LC_MESSAGES/sphinxcontrib.serializinghtml.mo" check $P/locales-ja-mo test -s "$PKGDIR/locales/ja/LC_MESSAGES/sphinxcontrib.serializinghtml.mo" check $P/locales-ru-po test -s "$PKGDIR/locales/ru/LC_MESSAGES/sphinxcontrib.serializinghtml.po" # --- purity ------------------------------------------------------------------ # Both `find' cases would pass VACUOUSLY against a directory that is not there, # so each is preceded by the existence case that makes its emptiness mean # something (the package-dir case above serves the first). check_eq $P/no-so "" "$(find "$PKGDIR" -name '*.so' 2>/dev/null)" check $P/lib-dynload-dir test -d /usr/lib/python3.12/lib-dynload check_eq $P/lib-dynload-empty "" \ "$(find /usr/lib/python3.12/lib-dynload -name '*.so' 2>/dev/null)" # --- the shipped file list --------------------------------------------------- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # This distribution declares no console script, so it must install no command. # An entry here would mean upstream had grown one and the recipe had shipped it # unnoticed. check_eq $P/pkg-no-bin "0" "$(grep -cE '^/usr/bin/' "$W/pkgf" 2>/dev/null)" # --- dist-info --------------------------------------------------------------- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -s "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-wheel test -s "$DI/WHEEL" check $P/dist-info-license test -s "$DI/LICENSE" # AND the file that is NOT there. Unlike python3-sphinx and unlike # sphinxcontrib-jquery, this distribution declares no entry point of any kind -- # pyproject.toml has no [project.entry-points] table -- so flit writes no # entry_points.txt. Sphinx finds this extension by IMPORT PATH, from its own # builtin_extensions list, not through metadata discovery. The ticket's # "entry-point metadata asserted as for the other extensions" is answered here, # and the answer is that there is none. check_eq $P/dist-info-no-entry-points "" "$(ls "$DI/entry_points.txt" 2>/dev/null)" check_eq $P/dist-info-record-lines "116" "$(wc -l < "$DI/RECORD" | tr -d ' ')" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-generator "Generator: flit 3.9.0" cat "$DI/WHEEL" check_out $P/dist-info-name "Name: sphinxcontrib-serializinghtml" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 1.1.10" cat "$DI/METADATA" check_out $P/dist-info-requires-python "Requires-Python: >=3.9" cat "$DI/METADATA" check_out $P/dist-info-framework-sphinx "Classifier: Framework :: Sphinx :: Extension" cat "$DI/METADATA" # UPSTREAM DECLARES SPHINX AS AN OPTIONAL EXTRA WHILE ITS MODULE BODY IMPORTS IT # UNCONDITIONALLY. That disagreement is the reason the bbappend's refusal to add # an RDEPENDS on python3-sphinx is defensible on upstream's own terms as well as # on the cycle's, so both halves are pinned: the extra is asserted here, and the # unconditional import is asserted by `probe-import-serializinghtml' below. check_out $P/meta-sphinx-standalone-extra \ "Requires-Dist: Sphinx>=5 ; extra == \"standalone\"" cat "$DI/METADATA" check_out $P/meta-provides-extra-standalone "Provides-Extra: standalone" cat "$DI/METADATA" # The unconditional requirement set is EMPTY: every Requires-Dist line carries an # `extra ==' marker. Counting is what says that, rather than the presence of any # one line. check_eq $P/meta-requires-count "5" \ "$(grep -c '^Requires-Dist:' "$DI/METADATA" | tr -d ' ')" check_eq $P/meta-requires-unconditional "0" \ "$(grep '^Requires-Dist:' "$DI/METADATA" | grep -vc 'extra ==' | tr -d ' ')" # --- THE RDEPENDS CORRECTION, as a DECLARATION ------------------------------- # poky's recipe declares NO target RDEPENDS at all, so the stanza it produces # read `Depends: python3-core' and claimed this package needs nothing from the # standard library beyond the core split -- while __init__.py imports pickle and # jsonimpl imports json, both at module scope and both dereferenced during class # definition. The bbappend beside this suite adds those two and nothing else; # every other module-scope import (os, os.path, types, typing, # collections.UserString) is in python3-core, checked against poky's # python3-manifest.json. Reading the stanza from dpkg rather than from the recipe # is deliberate: it is what the image actually installed. dpkg-query -f '${Depends}' -W python3-sphinxcontrib-serializinghtml > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/depends-readable test -s "$W/dep" check_out $P/depends-python3-core "python3-core" cat "$W/dep" check_out $P/depends-python3-pickle "python3-pickle" cat "$W/dep" check_out $P/depends-python3-json "python3-json" cat "$W/dep" # AND THE ENTRY THAT IS DELIBERATELY ABSENT. python3-sphinx is what this module # actually needs to import, and declaring it would be a CYCLE: poky's # python3-sphinx_7.2.6.bb RDEPENDS on this package, which Sphinx loads as one of # its five builtin extensions. It is also unnecessary -- python3-sphinx is on # core-image-brintos-emcraft's roster in its own right and pulls this package in # with it. This case pins the decision so that adding the entry becomes a # deliberate act with a red beside it, not a quiet edit. check_eq $P/pkg-depends-no-sphinx "0" \ "$(grep -c 'python3-sphinx[, ]\|python3-sphinx$' "$W/dep" | tr -d ' ')" # --- the probe: one interpreter start, every runtime question ---------------- PROBE_OUT=$W/probe.out check $P/probe-present test -s "$PROBE" ( python3 "$PROBE" > "$PROBE_OUT" 2>"$W/probe.err" ) check $P/probe-produced-output test -s "$PROBE_OUT" field() { sed -n "s/^$1 //p" "$PROBE_OUT" | head -1; } # THE CASE THE PORT TURNS ON. If this is an EXC, the module is unimportable # again and every case below it reports an empty field -- which reds rather than # passing vacuously, and says so at the top. check_eq $P/probe-import-serializinghtml "ok" "$(field import-serializinghtml)" check_eq $P/probe-version "1.1.10" "$(field version)" check_eq $P/probe-version-info "1.1.10" "$(field version-info)" check_eq $P/probe-under-site-packages "yes" "$(field module-file-under-site-packages)" check_eq $P/probe-last-build-filename "last_build" "$(field last-build-filename)" # The submodule by its own import path -- the import that used to fail even # though jsonimpl is pure stdlib, because it executes the package __init__ first. check_eq $P/probe-import-jsonimpl "ok" "$(field import-jsonimpl)" check_eq $P/probe-jsonimpl-is-attr "yes" "$(field jsonimpl-is-module-attr)" # The PEP 420 namespace, measured rather than inferred from the absent file. check_eq $P/probe-namespace-no-file "yes" "$(field namespace-package-no-file)" check_eq $P/probe-namespace-path-site-packages "yes" "$(field namespace-path-has-site-packages)" # --- the two builders, as declared ------------------------------------------ check_eq $P/probe-json-name "json" "$(field json-builder-name)" check_eq $P/probe-pickle-name "pickle" "$(field pickle-builder-name)" check_eq $P/probe-json-suffix ".fjson" "$(field json-out-suffix)" check_eq $P/probe-pickle-suffix ".fpickle" "$(field pickle-out-suffix)" check_eq $P/probe-json-globalctx "globalcontext.json" "$(field json-globalcontext)" check_eq $P/probe-pickle-globalctx "globalcontext.pickle" "$(field pickle-globalcontext)" check_eq $P/probe-json-searchindex "searchindex.json" "$(field json-searchindex)" check_eq $P/probe-pickle-searchindex "searchindex.pickle" "$(field pickle-searchindex)" # WHICH serializer each builder is wired to, by identity rather than by name: a # builder pointing at the wrong module would still have a plausible name. check_eq $P/probe-json-impl-is-jsonimpl "yes" "$(field json-implementation-is-jsonimpl)" check_eq $P/probe-pickle-impl-is-pickle "yes" "$(field pickle-implementation-is-pickle)" check_eq $P/probe-json-indexer-is-jsonimpl "yes" "$(field json-indexer-format-is-jsonimpl)" check_eq $P/probe-pickle-indexer-is-pickle "yes" "$(field pickle-indexer-format-is-pickle)" # Text vs binary, which is what decides whether dump_context() opens the output # file in "w" or "wb" -- the difference between a readable .fjson and a TypeError. check_eq $P/probe-json-dumps-unicode "yes" "$(field json-dumps-unicode)" check_eq $P/probe-pickle-dumps-unicode "no" "$(field pickle-dumps-unicode)" check_eq $P/probe-json-dump-args "0" "$(field json-additional-dump-args-count)" check_eq $P/probe-pickle-dump-args "1" "$(field pickle-additional-dump-args-count)" # The pickle builder writes at the interpreter's HIGHEST_PROTOCOL, 5 on CPython # 3.12. Pinned as a number AND as an identity: the number is what another program # reading the .fpickle files must support, and the identity is the contract. check_eq $P/probe-pickle-protocol "5" "$(field pickle-protocol)" check_eq $P/probe-pickle-protocol-is-highest "yes" "$(field pickle-protocol-is-highest)" # The inheritance that made this package unimportable without Sphinx, asserted # against the REAL sphinx.builders.html now on this image. check_eq $P/probe-subclass-serializing "yes" "$(field subclasses-serializing-builder)" check_eq $P/probe-subclass-standalone-html "yes" "$(field subclasses-standalone-html)" check_eq $P/probe-image-types "image/svg+xml,image/png,image/gif,image/jpeg" \ "$(field supported-image-types)" # get_target_uri()'s whole contract: the master document maps to the empty # string, a directory index loses its `index', anything else gains a separator. # The empty answer is emitted as `' by the probe, because an empty field # is also what a probe that died prints. check_eq $P/probe-target-uri-index "" "$(field target-uri-index)" check_eq $P/probe-target-uri-dir-index "guide/" "$(field target-uri-dir-index)" check_eq $P/probe-target-uri-plain "guide/topic/" "$(field target-uri-plain)" # --- setup(), the extension contract ---------------------------------------- # Asserted against a STUB application that records what it is asked to do. # A stub rather than the real app because Sphinx loads this package as a BUILTIN # extension at application construction, so a second setup() on a real app would # raise `builder json already registered' -- an artefact of the test rather than # a fact about the port. check_eq $P/probe-setup-called "ok" "$(field setup-called)" check_eq $P/probe-setup-returns-dict "yes" "$(field setup-returns-dict)" check_eq $P/probe-setup-version "1.1.10" "$(field setup-version)" check_eq $P/probe-setup-parallel-read "yes" "$(field setup-parallel-read-safe)" check_eq $P/probe-setup-parallel-write "yes" "$(field setup-parallel-write-safe)" check_eq $P/probe-setup-keys "parallel_read_safe,parallel_write_safe,version" \ "$(field setup-keys)" check_eq $P/probe-setup-require-sphinx "5.0" "$(field setup-require-sphinx)" check_eq $P/probe-setup-extension "sphinx.builders.html" "$(field setup-extension)" # The ORDER matters as much as the set: JSON is registered first. check_eq $P/probe-setup-builders "json,pickle" "$(field setup-builders)" check_eq $P/probe-setup-builders-are-classes "yes" "$(field setup-builders-are-the-classes)" check_eq $P/probe-setup-catalog-name "sphinxcontrib.serializinghtml" \ "$(field setup-catalog-name)" check_eq $P/probe-setup-catalog-dir-exists "yes" "$(field setup-catalog-dir-exists)" check_eq $P/probe-setup-catalog-under-package "yes" "$(field setup-catalog-under-package)" # --- the serializers, round-tripped ----------------------------------------- # Over a context dict carrying what Sphinx actually puts in one: strings, a # rendered HTML fragment, nested dicts and lists, a bool, a None, non-ASCII text, # and the translation proxy SphinxJSONEncoder exists for. check_eq $P/probe-json-roundtrip "yes" "$(field json-dumps-loads-roundtrip)" check_eq $P/probe-json-proxy-encoded "yes" "$(field json-proxy-encoded)" # The negative that makes the case above mean something: without the encoder the # same dict is a TypeError, so the encoder is doing work rather than being # redundant. check_eq $P/probe-json-proxy-needs-encoder "yes" \ "$(field json-proxy-would-fail-without-encoder)" # A writer whose output only its own reader can read would satisfy the round-trip # and mean nothing. THIS is the case that says .fjson is JSON. check_eq $P/probe-json-read-by-stdlib "yes" "$(field json-read-by-stdlib)" check_eq $P/probe-pickle-roundtrip "yes" "$(field pickle-dumps-loads-roundtrip)" check_eq $P/probe-json-file-roundtrip "yes" "$(field json-file-roundtrip)" check_eq $P/probe-json-file-read-by-stdlib "yes" "$(field json-file-read-by-stdlib)" check_eq $P/probe-pickle-file-roundtrip "yes" "$(field pickle-file-roundtrip)" # --- two REAL documentation builds ------------------------------------------ # The cases that separate "the module imports" from "the builders work": config # load, extension setup, docutils parse, environment pickle, and the serialised # per-page output written, read back and compared with what went in. Two # documents, so the toctree and the target URIs are exercised rather than # asserted. check_eq $P/probe-build-json-builder-name "json" "$(field build-json-builder-name)" check_eq $P/probe-build-json-builder-class "JSONHTMLBuilder" "$(field build-json-builder-class)" check_eq $P/probe-build-json "ok" "$(field build-json)" check_eq $P/probe-build-json-index "yes" "$(field build-json-has-index)" check_eq $P/probe-build-json-guide-index "yes" "$(field build-json-has-guide-index)" check_eq $P/probe-build-json-globalcontext "yes" "$(field build-json-has-globalcontext)" check_eq $P/probe-build-json-searchindex "yes" "$(field build-json-has-searchindex)" # handle_finish() touches `last_build' and copies the environment pickle into the # output directory, both for the web application that consumes this format; they # are the two files that distinguish a serialising build from an HTML one. check_eq $P/probe-build-json-last-build "yes" "$(field build-json-has-last-build)" check_eq $P/probe-build-json-environment "yes" "$(field build-json-has-environment)" check_eq $P/probe-build-json-sources "yes" "$(field build-json-has-sources)" # The page itself: parsed by the interpreter's OWN json module, with the fields # a consumer reads. check_eq $P/probe-build-json-page-parsed "ok" "$(field build-json-page-parsed-by-stdlib)" check_eq $P/probe-build-json-page-title "bst probe" "$(field build-json-page-title)" check_eq $P/probe-build-json-page-name "index" "$(field build-json-page-current-name)" check_eq $P/probe-build-json-page-emphasis "yes" "$(field build-json-page-has-emphasis)" check_eq $P/probe-build-json-page-literal "yes" "$(field build-json-page-has-literal)" check_eq $P/probe-build-json-page-sourcename "index.rst.txt" "$(field build-json-page-sourcename)" # handle_page() deletes every function from the context before dumping, which is # what makes the result serialisable at all. check_eq $P/probe-build-json-page-no-callables "yes" "$(field build-json-page-no-callables)" check_eq $P/probe-build-json-globalcontext-project "bst" \ "$(field build-json-globalcontext-project)" check_eq $P/probe-build-json-searchindex-docnames-key "yes" \ "$(field build-json-searchindex-has-docnames)" check_eq $P/probe-build-json-searchindex-docnames "guide/index,index" \ "$(field build-json-searchindex-docnames)" check_eq $P/probe-build-pickle-builder-name "pickle" "$(field build-pickle-builder-name)" check_eq $P/probe-build-pickle-builder-class "PickleHTMLBuilder" "$(field build-pickle-builder-class)" check_eq $P/probe-build-pickle "ok" "$(field build-pickle)" check_eq $P/probe-build-pickle-index "yes" "$(field build-pickle-has-index)" check_eq $P/probe-build-pickle-guide-index "yes" "$(field build-pickle-has-guide-index)" check_eq $P/probe-build-pickle-globalcontext "yes" "$(field build-pickle-has-globalcontext)" check_eq $P/probe-build-pickle-searchindex "yes" "$(field build-pickle-has-searchindex)" check_eq $P/probe-build-pickle-last-build "yes" "$(field build-pickle-has-last-build)" check_eq $P/probe-build-pickle-environment "yes" "$(field build-pickle-has-environment)" check_eq $P/probe-build-pickle-page-loaded "ok" "$(field build-pickle-page-loaded)" check_eq $P/probe-build-pickle-page-title "bst probe" "$(field build-pickle-page-title)" check_eq $P/probe-build-pickle-page-emphasis "yes" "$(field build-pickle-page-has-emphasis)" # THE TWO BUILDERS MUST AGREE. Same source, same context, two serialisers: the # rendered body is what a consumer of either format reads, and it is the one # place a serialiser-specific mangling would show. check_eq $P/probe-build-formats-agree-body "yes" "$(field build-formats-agree-on-body)" check_eq $P/probe-build-formats-agree-title "yes" "$(field build-formats-agree-on-title)" # The backstop for a failure in either build that is not one of the cases above: # an exception out of the build block is its own case precisely so that a failure # in the SECOND build cannot hide behind the first one's answer. check_eq $P/probe-build-no-exception "" "$(field build-exception)" check_eq $P/probe-complete "yes" "$(field probe-complete)" rm -rf "$W"