-
Notifications
You must be signed in to change notification settings - Fork 263
cfx py
Brian Warner, @warner 03-Apr-2012
(note: this describes the state of Jetpack as of early April 2012, release 1.6 or 1.7, git revision [c37bf47de25551832b96a53d9cd15efe8bfa7c11] (https://github.com/mozilla/addon-sdk/tree/c37bf47de25551832b96a53d9cd15efe8bfa7c11). Things change fast in the SDK, and this document will probably be obsolete within a few months).
The Jetpack Addon-SDK contains a library of CommonJS modules (most of which provide access to specific browser APIs), and a "linker" which combines a loader module, user-written JS source code, and any referenced library modules. The linker outputs an .xpi file, ready for installation into a browser.
"cfx" is the main tool provided by the Addon-SDK. Users first interact with cfx by running "cfx init", which creates a skeleton addon source directory. After modifying the code, users run the "cfx xpi" subcommand in their source directory to run the linker and produce an .xpi file. When run as "cfx run", it produces an .xpi and immediately launches a browser with a new (generally ephemeral) profile, with the addon already installed. When run as "cfx test", it does the same, but executes the addon's unit test suite, then shuts down.
cfx has other modes that are less addon-specific. "cfx testall" runs the SDK's internal test suite, including tests on all library modules ("cfx testpkgs"), example code ("cfx testex"), and tests of cfx itself ("cfx testcfx"). "cfx docs" opens a browser window against a small built-in webserver, showing the rendered markdown documentation (both in docs/ and in packages/*/docs/). "cfx sdocs" renders this documentation into a static tarball full of HTML files, for copying to a normal webserver (like MDN).
cfx is implemented in Python, and the name is derived from "cuddlefish executable", where "cuddlefish" is the name of both the python package (SDK/python-lib/cuddlefish) that contains the bulk of the code, and the Javascript module (SDK/packages/api-utils/lib/cuddlefish.js) that implements most of the loader. "cuddlefish" derives from an amusing misspelling of "cuttlefish", an intriguing and potentially world-threatening species of fish.
The entry point is in bin/cfx, but that merely loads the right code and
transfers control to SDK/python-lib/cuddlefish/__init__.py
.
-
SDK/bin/cfx
: entry point -
SDK/python-lib/cuddlefish
: most cfx code lives here - third-party modules:
-
SDK/python-lib/markdown
: markdown->HTML renderer -
SDK/python-lib/mozrunner
: old snapshot of mozrunner, for 'cfx run' -
SDK/python-lib/simplejson
: JSON parser
"simplejson" is used in lieu of python's stdlib "json" module, because stdlib didn't include it until python2.6, and the SDK is supposed to be compatible with python2.5. Our "mozrunner" snapshot contains local modifications and has diverged a bit from the upstream (https://github.com/mozilla/mozbase).
-
tests/
contains unit tests, run by 'cfx testcfx' -
app-extension/
contains the initial XPI contents, including a template for install.rdf and a copy of bootstrap.js, both of which serve as the XPI's entry point. -
mobile-utils/
contains similar XPI contents for mobile addons. -
docs/
contains parsers and renderers for the SDK's extended markdown format. These build on top of the third-party "markdown" library. -
_version.py
is used to automatically determine the SDK's version from git metadata. "cfx --version" reports this detailed version information. -
bunch.py
is a small utility class to make python dictionaries behave more like Javascript objects (mybunch.foo == mybunch["foo"]). -
util.py
holds some small functions to ignore boring files (dotfiles, editor tempfiles, version-control metadata) when scanning directories or building the XPI. -
version_comparator.py
is unused -
templates.py
holds the contents of the skeleton addon created by "cfx init" -
options_defaults.py
parses the "preferences" property of package.json -
options_xul.py
renders these parsed preferences into an options.xul file inside the XPI. -
property_parser.py
parses locale data from .properties files, for inclusion in XPI files like "locale/LANGUAGE.json". -
prefs.py
contains preferences which are installed into ephemeral "cfx run" profiles, mostly to turn on debugging tools and prevent the new browser from doing developer-hostile things like rejecting third-party-installed addons and immediately looking for updates. -
rdf.py
helps create the XPI's install.rdf by incorporating addon-specific data into the template from app-extension/ . -
preflight.py
makes randomly-generated "JID" addon identifiers. Long ago, JIDs were actually public keys, and the pre-flight code ensured that each addon had a corresponding private key in ~/.jetpack, generating a keypair if necessary. That was removed before 1.0 shipped.
The bulk of cfx lives in 5 files:
__init__.py
manifest.py
packaging.py
runner.py
xpi.py
The first half of __init__.py
contains options-parsers (using the stdlib
"optparse" module) that populate the "options" object, and utility functions
that drive the various unit-test modes. There is also a helper function to
initialize a skeleton addon (invoked by "cfx init"). The other half of
__init__.py
is the large run() function.
SDK/bin/cfx transfers control to this run()
. Most of the non-XPI-building
subcommands (init, test, docs) are dispatched early. Once we're committed to
building an XPI, the next step is to scan the top-level package.json and the
contents of our packages/ directory.
We use the packaging.py module (packaging.get_config_in_dir()
) to scan the
top-level package.json file and create a data structure named "target_cfg".
The actual return value is a "Bunch" (basically just a dictionary with more
JS-like accessor properties). We'll call this kind of Bunch (returned by
get_config_in_dir()
) a "package descriptor". Note that this function only
handles the main add-on source code (in the current directory), not the rest
of packages/ (those are handled later). The function looks briefly in the
package for some well-known directories ("tests" vs "test", etc), and for
some icon files, but in general the only file it actually reads is
package.json. It remembers the directory that the package lives in, as
target_cfg.root_dir
. Any keys in the package.json are copied to properties
of target_cfg
. We'll call
Back in __init__.py
, we run some additional code that differentiates
between the "xpi", "test", and "run" subcommands, specifically to figure out
if we need to look for a "main" property or not. Then we delegate back to
packaging.build_config()
to find the rest of the packages.
build_config()
performs a recursive scan for packages. It starts with
SDK/packages/ and anything added to the --package-path
. For each one, it
finds all the subdirectories with package.json files and processes each
subdirectory as a package. If the package contains a packages/
subdirectory, or names some other subdirectory via a .packages
property in
its package.json file, then that subdirectory will be scanned too.
build_config()
returns a pkg_cfg
with the full set of known packages in
pkg_cfg.packages
(so a package named "foo" will be described by
pkg_cfg.packages["foo"]
). Each package descriptor is created by
get_config_in_dir()
as above. Note that name collisions between packages
are not allowed: build_config()
will throw an exception if this happens.
Back in __init__.py
, the code now calls preflight_config()
to make sure
the JID and (what used to be) other data in package.json is ready. If the
preflight checks fail, cfx prints a message (telling the user to fix the
problem and then try again), then cfx is halted. This used to be necessary
when the JID was a pubkey; in some situations the user needed to edit their
package.json to change the key. Now that we no longer use keys, this code
should be replaced (bug 613587) by having "cfx init" create a JID earlier,
and the preflight check can merely assert that the addon has a legal JID.
Next, run() calls packaging.get_deps_for_target()
to build a list of
required packages for this addon. This uses package.json .dependencies
and
.extra_dependencies
properties to traverse the package-dependency graph. It
also includes SDK/packages/test-harness if the "test" subcommand is
executing. The results are stored in "deps", as a list of package
descriptors.
The next step is to build the manifest. run() first locates the
cuddlefish.js
loader module, and determines if "cfx test" is being run (to
tell the manifest scanner to look for tests too). This it imports
manifest.py
and invokes the build_manifest()
function, which returns a
Manifest object. This process is examined in more detail below.
After generating the manifest, run() calls
packaging.generate_build_for_target
. This basically generates a list of
directories ("sections") that need to be included in the XPI, in
build.packages[pkgname][section]
. It also builds a list of locale objects,
and populates a few properties (icons, preferences) that need to be stored in
harness-options.json.
Then run() creates the main harness_options
object, filling in metadata
pieces like the JID, the addon's name, entry point names, the sdkVersion, and
the manifest itself. Then it calls rdf.gen_manifest
to create the "RDF
manifest" (i.e. install.rdf
), which contains some of this metadata. If
there is an update URL (for automatic updates of the addon), it stashes this
into install.rdf too.
The next step depends upon the subcommand being run. For "cfx xpi", it asks
the manifest for a list of used files, then calls xpi.build_xpi
to create
the zipfile and fill it with both synthesized metadata files and copies of
files from user code and the packages/ library. For both "cfx run" and "cfx
test", it delegates to runner.run_app
to create a new profile (or reuse an
existing one), build an XPI as with "cfx xpi", install the addon into the new
profile, then launch firefox (or other application).
build_xpi()
is a fairly straightforward recursive copy into a temporary
directory, which is then bundled into a zipfile with a .xpi suffix. The
current code walks through all packages and sections, doing a local os.walk()
scan for files in its directory, copying everything that is listed in the
manifest into the zipfile. It also copies locale files in. One proposed
rewrite (by warner, not completed) changed this process to have the manifest
create a "compile map" as a side-effect of its normal scan: a mapping from
local-disk filename to XPI filename, then have xpi.py strictly follow this
mapping to build the XPI. A reverse map named the "decompile map" would allow
a hypothetical "cfx unpack" command to build a source tree out of the
contents of the XPI, potentially making an XPI-repacker service more robust
against changes to the XPI layout.
run_app()
is built on top of mozrunner. It adds code to handle Fennec
remote-debugger launching better, and to communicate with child browsers
through logfiles (since apparently Gecko doesn't make it easy to write to
stderr, and pipes on Windows are always troublesome). It is responsible for
injecting the default preferences from prefs.py
into the new profile. It
includes a call to build_xpi
, rather than using a single common build_xpi
in __init__.py
, because the filename of the logfile is passed to the addon
via harness-options.json. If/when this is changed to pass through an
environment variable instead, the local call should be replaced by a single
common build_xpi()
call.
"cfx test" is handled through the "cfx run" case, but the XPI generated (in particular the choice of entry point) will execute the test runner instead of the normal main() function.
Finally, when run_app()
or build_xpi()
returns, run()
calls
sys.exit()
, and cfx terminates.
manifest.ManifestBuilder
performs a recursive traversal of the module
graph. It starts in the build()
method, where it processes the entry points
(the main.js
module, any test files that weren't filtered away, and any
extra loader modules). Each entry point can reference other modules which
reference still more modules, etc. The process accumulates information about
each module as instance attributes on the ManifestBuilder
. Each module gets
a ModuleInfo
instance. The manifest itself (a list of (X,Y,Z) tuples that
mean "when module X says require(Y), give it module Z") is stored as a
numbered list of ManifestEntry
instances. The code allocates a manifest
entry before descending into the about-to-be-processed module, to tolerate
cycles.
To process each module, ManifestBuilder.process_module()
uses a regexp
(scan_requirements_with_grep
) to identify require()
statements and
extract the name of the module being imported in each. It then figures out
which file will satisfy the import (ManifestBuilder.find_req_for()
) by
evaluating relative pathnames and searching the package list. Modules that
already exist in the manifest are skipped (terminating cycles). New modules
are processed, recursively.
find_req_for
and related methods (_get_module_from_package
,
_get_entrypoint_from_package
, _search_packages_for_module
,
_find_module_in_package
) spend a lot of time and complexity implementing
the require()
lookup functions: this is the compile-time module search
algorithm. Any changes to the way that require() should work will need to
modify these functions, hopefully to simplify them.
Any time a require(self)
statement is encountered, a DataMap
object for
the current package is created (or pulled from the cache). This holds a list
of all data files, along with their filenames and hashes.
Later, when the XPI is built, several ManifestBuilder
accessor methods are
used to extract information about what to put into the zipfile.
get_used_files
provides a list of all the .js and data files used by the
addon: this is used to filter unused files out of the XPI.
get_harness_options_manifest
is used to get the primary JSONable manifest
structure for storage in harness-options.json.