Bug 935305 - Move preprocessor to mozbuild.action. r=gps
authorMike Hommey <mh+mozilla@glandium.org>
Sat, 09 Nov 2013 10:35:44 +0900
changeset 154232 484945495f95ca30fea9e096a5fabbf49b18e787
parent 154231 4810e8f4eb98462aa7134d691d5dc240163e2f53
child 154233 26ffa28bd6976fe8cc9391cf9be77a6ae9c8a945
push id36021
push usermh@glandium.org
push dateSat, 09 Nov 2013 01:37:36 +0000
treeherdermozilla-inbound@37698b2e2330 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs935305
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 935305 - Move preprocessor to mozbuild.action. r=gps
b2g/installer/Makefile.in
b2g/locales/Makefile.in
browser/app/Makefile.in
browser/installer/Makefile.in
browser/installer/windows/Makefile.in
browser/locales/Makefile.in
browser/metro/Makefile.in
browser/metro/locales/import/Makefile.in
build/Makefile.in
build/automation-build.mk
build/docs/preprocessor.rst
config/Expression.py
config/Makefile.in
config/Preprocessor.py
config/rules.mk
config/tests/unit-Expression.py
config/tests/unit-LineEndings.py
config/tests/unit-Preprocessor.py
db/sqlite3/src/Makefile.in
dom/bindings/Makefile.in
js/src/Makefile.in
js/src/config/Expression.py
js/src/config/Makefile.in
js/src/config/Preprocessor.py
js/src/config/rules.mk
js/xpconnect/src/Makefile.in
js/xpconnect/tests/components/js/Makefile.in
layout/mathml/Makefile.in
layout/media/Makefile.in
mobile/android/base/Makefile.in
mobile/android/base/locales/Makefile.in
mobile/android/installer/Makefile.in
mobile/locales/Makefile.in
modules/libmar/tests/Makefile.in
modules/libpref/src/Makefile.in
mozglue/build/Makefile.in
python/mozbuild/mozbuild/action/preprocessor.py
python/mozbuild/mozbuild/backend/configenvironment.py
python/mozbuild/mozbuild/jar.py
python/mozbuild/mozbuild/preprocessor.py
python/mozbuild/mozbuild/test/test_expression.py
python/mozbuild/mozbuild/test/test_line_endings.py
python/mozbuild/mozbuild/test/test_preprocessor.py
python/mozbuild/mozpack/packager/__init__.py
python/mozbuild/mozpack/test/test_packager.py
services/sync/Makefile.in
toolkit/library/Makefile.in
toolkit/locales/l10n.mk
toolkit/mozapps/installer/packager.mk
webapprt/Makefile.in
webapprt/win/Makefile.in
xulrunner/app/Makefile.in
xulrunner/installer/Makefile.in
--- a/b2g/installer/Makefile.in
+++ b/b2g/installer/Makefile.in
@@ -47,17 +47,17 @@ endif
 DEFINES += -DBINPATH=$(BINPATH)
 
 ifneq (,$(filter WINNT Darwin Android,$(OS_TARGET)))
 DEFINES += -DMOZ_SHARED_MOZGLUE=1
 endif
 
 ifdef MOZ_PKG_MANIFEST_P
 $(MOZ_PKG_MANIFEST): $(MOZ_PKG_MANIFEST_P) FORCE
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) $< -o $@)
 ifdef MOZ_CHROME_MULTILOCALE
 	printf "\n[multilocale]\n" >> $@
 	for LOCALE in $(MOZ_CHROME_MULTILOCALE) ;\
 	do \
 	  printf "$(BINPATH)/chrome/$$LOCALE$(JAREXT)\n" >> $@; \
 	  printf "$(BINPATH)/chrome/$$LOCALE.manifest\n" >> $@; \
 	done
 endif
--- a/b2g/locales/Makefile.in
+++ b/b2g/locales/Makefile.in
@@ -49,18 +49,18 @@ include $(topsrcdir)/toolkit/locales/l10
 
 $(STAGEDIST): $(DIST)/branding
 
 $(DIST)/branding:
 	$(NSINSTALL) -D $@
 
 libs::
 	@if test -f "$(LOCALE_SRCDIR)/existing-profile-defaults.js"; then \
-	  $(PYTHON) $(topsrcdir)/config/Preprocessor.py $(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) \
-	    $(LOCALE_SRCDIR)/existing-profile-defaults.js > $(FINAL_TARGET)/defaults/existing-profile-defaults.js; \
+	  $(PYTHON) -m mozbuild.action.preprocessor $(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) \
+	    $(LOCALE_SRCDIR)/existing-profile-defaults.js -o $(FINAL_TARGET)/defaults/existing-profile-defaults.js; \
 	fi
 
 NO_JA_JP_MAC_AB_CD := $(if $(filter ja-JP-mac, $(AB_CD)),ja,$(AB_CD))
 
 libs-%:
 	$(NSINSTALL) -D $(DIST)/install
 	@$(MAKE) -C ../../toolkit/locales libs-$*
 	@$(MAKE) -C ../../intl/locales AB_CD=$* XPI_NAME=locale-$*
--- a/browser/app/Makefile.in
+++ b/browser/app/Makefile.in
@@ -139,17 +139,17 @@ endif
 
 libs:: $(srcdir)/profile/prefs.js
 	$(INSTALL) $(IFLAGS1) $^ $(FINAL_TARGET)/defaults/profile
 
 ifndef LIBXUL_SDK
 # channel-prefs.js is handled separate from other prefs due to bug 756325
 libs:: $(srcdir)/profile/channel-prefs.js
 	$(NSINSTALL) -D $(DIST)/bin/defaults/pref
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(PREF_PPFLAGS) $(ACDEFINES) $^ > $(DIST)/bin/defaults/pref/channel-prefs.js
+	$(call py_action,preprocessor,$(PREF_PPFLAGS) $(ACDEFINES) $^ -o $(DIST)/bin/defaults/pref/channel-prefs.js)
 endif
 
 libs:: $(srcdir)/blocklist.xml
 	$(INSTALL) $(IFLAGS1) $^ $(FINAL_TARGET)
 
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 
 MAC_APP_NAME = $(MOZ_APP_DISPLAYNAME)
--- a/browser/installer/Makefile.in
+++ b/browser/installer/Makefile.in
@@ -74,17 +74,17 @@ endif
 ifdef NECKO_WIFI
 DEFINES += -DNECKO_WIFI
 endif
 
 ifdef MOZ_PKG_MANIFEST_P
 MOZ_PKG_MANIFEST = package-manifest
 
 $(MOZ_PKG_MANIFEST): $(MOZ_PKG_MANIFEST_P) $(GLOBAL_DEPS)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) $< -o $@)
 
 GARBAGE += $(MOZ_PKG_MANIFEST)
 endif
 
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 MOZ_PKG_MAC_DSSTORE=branding/dsstore
 MOZ_PKG_MAC_BACKGROUND=branding/background.png
 MOZ_PKG_MAC_ICON=branding/disk.icns
--- a/browser/installer/windows/Makefile.in
+++ b/browser/installer/windows/Makefile.in
@@ -66,40 +66,40 @@ installer::
 
 # For building the uninstaller during the application build so it can be
 # included for mar file generation.
 uninstaller::
 	$(RM) -r $(CONFIG_DIR)
 	$(MKDIR) $(CONFIG_DIR)
 	$(INSTALL) $(addprefix $(srcdir)/,$(INSTALLER_FILES)) $(CONFIG_DIR)
 	$(INSTALL) $(addprefix $(DIST)/branding/,$(BRANDING_FILES)) $(CONFIG_DIR)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py -Fsubstitution $(DEFINES) $(ACDEFINES) \
-	  $(srcdir)/nsis/defines.nsi.in > $(CONFIG_DIR)/defines.nsi
+	$(call py_action,preprocessor,-Fsubstitution $(DEFINES) $(ACDEFINES) \
+	  $(srcdir)/nsis/defines.nsi.in -o $(CONFIG_DIR)/defines.nsi)
 	$(PYTHON) $(topsrcdir)/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py \
 	  --preprocess-locale $(topsrcdir) \
 	  $(PPL_LOCALE_ARGS) $(AB_CD) $(CONFIG_DIR)
 
 # For building the maintenanceservice installer
 ifdef MOZ_MAINTENANCE_SERVICE
 maintenanceservice_installer::
 	$(INSTALL) $(addprefix $(srcdir)/,$(INSTALLER_FILES)) $(CONFIG_DIR)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py -Fsubstitution $(DEFINES) $(ACDEFINES) \
-	  $(srcdir)/nsis/defines.nsi.in > $(CONFIG_DIR)/defines.nsi
+	$(call py_action,preprocessor,-Fsubstitution $(DEFINES) $(ACDEFINES) \
+	  $(srcdir)/nsis/defines.nsi.in -o $(CONFIG_DIR)/defines.nsi)
 	$(PYTHON) $(topsrcdir)/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py \
 	  --preprocess-locale $(topsrcdir) \
 	  $(PPL_LOCALE_ARGS) $(AB_CD) $(CONFIG_DIR)
 endif
 
 $(CONFIG_DIR)/setup.exe::
 	$(RM) -r $(CONFIG_DIR)
 	$(MKDIR) $(CONFIG_DIR)
 	$(INSTALL) $(addprefix $(srcdir)/,$(INSTALLER_FILES)) $(CONFIG_DIR)
 	$(INSTALL) $(addprefix $(DIST)/branding/,$(BRANDING_FILES)) $(CONFIG_DIR)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py -Fsubstitution $(DEFINES) $(ACDEFINES) \
-	  $(srcdir)/nsis/defines.nsi.in > $(CONFIG_DIR)/defines.nsi
+	$(call py_action,preprocessor,-Fsubstitution $(DEFINES) $(ACDEFINES) \
+	  $(srcdir)/nsis/defines.nsi.in -o $(CONFIG_DIR)/defines.nsi)
 	$(PYTHON) $(topsrcdir)/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py \
 	  --preprocess-locale $(topsrcdir) \
 	  $(PPL_LOCALE_ARGS) $(AB_CD) $(CONFIG_DIR)
 	$(PYTHON) $(topsrcdir)/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py \
 	  --preprocess-single-file $(topsrcdir) \
 	  $(PPL_LOCALE_ARGS) $(CONFIG_DIR) \
 	  nsisstrings.properties nsisstrings.nlf
 
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -100,22 +100,21 @@ PROFILE_FILES = \
 	mimeTypes.rdf \
 	$(NULL)
 
 PROFILE_CHROME = userChrome-example.css userContent-example.css
 
 NO_JA_JP_MAC_AB_CD := $(if $(filter ja-JP-mac, $(AB_CD)),ja,$(AB_CD))
 
 %/defaults/profile/bookmarks.html: bookmarks.inc generic/profile/bookmarks.html.in
-	$(SYSINSTALL) -D $(dir $@)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
+	$(call py_action,preprocessor, \
 	  -I $< \
 	  -DAB_CD=$(NO_JA_JP_MAC_AB_CD) \
 	  $(srcdir)/generic/profile/bookmarks.html.in \
-	  > $@
+	  -o $@)
 
 libs:: $(FINAL_TARGET)/defaults/profile/bookmarks.html ;
 
 libs:: $(addprefix generic/profile/,$(PROFILE_FILES))
 	$(SYSINSTALL) $(IFLAGS1) $^ $(FINAL_TARGET)/defaults/profile
 
 libs:: $(call MERGE_FILES,$(addprefix profile/chrome/,$(PROFILE_CHROME)))
 	$(SYSINSTALL) $(IFLAGS1) $^ $(FINAL_TARGET)/defaults/profile/chrome
--- a/browser/metro/Makefile.in
+++ b/browser/metro/Makefile.in
@@ -12,16 +12,16 @@ DEFINES += -DMOZILLA_OFFICIAL
 endif
 
 GRE_BUILDID := $(shell cat $(DEPTH)/config/buildid)
 DEFINES += -DGRE_MILESTONE=$(GRE_MILESTONE) -DGRE_BUILDID=$(GRE_BUILDID)
 
 # 'application.ini' breaks firefox build config. So we use something different.
 metroapp.ini: metroapp.ini.in $(DEPTH)/config/buildid $(topsrcdir)/config/milestone.txt
 	$(RM) "metroapp.ini"
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $< > $@
+	$(call py_action,preprocessor,$(DEFINES) $< -o $@)
 
 libs:: metroapp.ini
 	$(INSTALL) metroapp.ini $(FINAL_TARGET)
 
 #########################################
 
 GARBAGE += metroapp.ini
--- a/browser/metro/locales/import/Makefile.in
+++ b/browser/metro/locales/import/Makefile.in
@@ -39,14 +39,14 @@ vpath book%.inc @top_srcdir@/$(relatives
 endif
 
 bookmarks-src = $(srcdir)/../generic/profile/bookmarks.json.in
 
 # The resulting bookmarks.json will get picked up and packaged by the
 # processing of the jar file in the parent directory.
 bookmarks: bookmarks.inc
 	@echo "Generating: $@"
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
+	$(call py_action,preprocessor, \
 	  -I $^ \
 	  -DAB_CD=$(AB_CD) \
-	  $(bookmarks-src) > ../bookmarks.json
+	  $(bookmarks-src) -o ../bookmarks.json)
 
 export:: bookmarks
--- a/build/Makefile.in
+++ b/build/Makefile.in
@@ -101,17 +101,17 @@ include $(srcdir)/automation-build.mk
 		$(topsrcdir)/build/pgo/blueprint/valid.png \
 		$(topsrcdir)/build/pgo/blueprint/screen.css \
 		$(topsrcdir)/build/pgo/blueprint/print.css \
 		$(topsrcdir)/build/pgo/blueprint/grid.png \
 		$(topsrcdir)/build/pgo/blueprint/fancytype-screen.css \
 		$(NULL)
 
 leaktest.py: leaktest.py.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $^ > $@
+	$(call py_action,preprocessor,$^ -o $@)
 	chmod +x $@
 GARBAGE += leaktest.py
 
 ifdef MOZ_APP_BASENAME
 $(FINAL_TARGET)/application.ini: $(APP_INI_DEPS)
 
 ifdef MOZ_APP_STATIC_INI
 DEFINES += -DMOZ_APP_STATIC_INI
--- a/build/automation-build.mk
+++ b/build/automation-build.mk
@@ -62,12 +62,12 @@ endif
 
 ifdef MOZ_ASAN
 AUTOMATION_PPARGS += -DIS_ASAN=1
 else
 AUTOMATION_PPARGS += -DIS_ASAN=0
 endif
 
 automation.py: $(MOZILLA_DIR)/build/automation.py.in $(MOZILLA_DIR)/build/automation-build.mk
-	$(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py \
-	$(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor, \
+	$(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< -o $@)
 
 GARBAGE += automation.py automation.pyc
--- a/build/docs/preprocessor.rst
+++ b/build/docs/preprocessor.rst
@@ -1,17 +1,17 @@
 .. _preprocessor:
 
 =================
 Text Preprocessor
 =================
 
 The build system contains a text preprocessor similar to the C preprocessor,
 meant for processing files which have no built-in preprocessor such as XUL
-and JavaScript documents. It is implemented at ``config/Preprocessor.py`` and
+and JavaScript documents. It is implemented at ``python/mozbuild/mozbuild/preprocessor.py`` and
 is typically invoked via :ref:`jar_manifests`.
 
 While used to preprocess CSS files, the directives are changed to begin with
 ``%`` instead of ``#`` to avoid conflict of the id selectors.
 
 Directives
 ==========
 
deleted file mode 100644
--- a/config/Expression.py
+++ /dev/null
@@ -1,241 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-"""
-Parses and evaluates simple statements for Preprocessor:
-
-Expression currently supports the following grammar, whitespace is ignored:
-
-expression :
-  and_cond ( '||' expression ) ? ;
-and_cond:
-  test ( '&&' and_cond ) ? ;
-test:
-  unary ( ( '==' | '!=' ) unary ) ? ;
-unary :
-  '!'? value ;
-value :
-  [0-9]+ # integer
-  | 'defined(' \w+ ')'
-  | \w+  # string identifier or value;
-"""
-
-import re
-
-class Expression:
-  def __init__(self, expression_string):
-    """
-    Create a new expression with this string.
-    The expression will already be parsed into an Abstract Syntax Tree.
-    """
-    self.content = expression_string
-    self.offset = 0
-    self.__ignore_whitespace()
-    self.e = self.__get_logical_or()
-    if self.content:
-      raise Expression.ParseError, self
-
-  def __get_logical_or(self):
-    """
-    Production: and_cond ( '||' expression ) ?
-    """
-    if not len(self.content):
-      return None
-    rv = Expression.__AST("logical_op")
-    # test
-    rv.append(self.__get_logical_and())
-    self.__ignore_whitespace()
-    if self.content[:2] != '||':
-      # no logical op needed, short cut to our prime element
-      return rv[0]
-    # append operator
-    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
-    self.__strip(2)
-    self.__ignore_whitespace()
-    rv.append(self.__get_logical_or())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_logical_and(self):
-    """
-    Production: test ( '&&' and_cond ) ?
-    """
-    if not len(self.content):
-      return None
-    rv = Expression.__AST("logical_op")
-    # test
-    rv.append(self.__get_equality())
-    self.__ignore_whitespace()
-    if self.content[:2] != '&&':
-      # no logical op needed, short cut to our prime element
-      return rv[0]
-    # append operator
-    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
-    self.__strip(2)
-    self.__ignore_whitespace()
-    rv.append(self.__get_logical_and())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_equality(self):
-    """
-    Production: unary ( ( '==' | '!=' ) unary ) ?
-    """
-    if not len(self.content):
-      return None
-    rv = Expression.__AST("equality")
-    # unary 
-    rv.append(self.__get_unary())
-    self.__ignore_whitespace()
-    if not re.match('[=!]=', self.content):
-      # no equality needed, short cut to our prime unary
-      return rv[0]
-    # append operator
-    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
-    self.__strip(2)
-    self.__ignore_whitespace()
-    rv.append(self.__get_unary())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_unary(self):
-    """
-    Production: '!'? value
-    """
-    # eat whitespace right away, too
-    not_ws = re.match('!\s*', self.content)
-    if not not_ws:
-      return self.__get_value()
-    rv = Expression.__AST('not')
-    self.__strip(not_ws.end())
-    rv.append(self.__get_value())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_value(self):
-    """
-    Production: ( [0-9]+ | 'defined(' \w+ ')' | \w+ )
-    Note that the order is important, and the expression is kind-of
-    ambiguous as \w includes 0-9. One could make it unambiguous by
-    removing 0-9 from the first char of a string literal.
-    """
-    rv = None
-    m = re.match('defined\s*\(\s*(\w+)\s*\)', self.content)
-    if m:
-      word_len = m.end()
-      rv = Expression.__ASTLeaf('defined', m.group(1))
-    else:
-      word_len = re.match('[0-9]*', self.content).end()
-      if word_len:
-        value = int(self.content[:word_len])
-        rv = Expression.__ASTLeaf('int', value)
-      else:
-        word_len = re.match('\w*', self.content).end()
-        if word_len:
-          rv = Expression.__ASTLeaf('string', self.content[:word_len])
-        else:
-          raise Expression.ParseError, self
-    self.__strip(word_len)
-    self.__ignore_whitespace()
-    return rv
-
-  def __ignore_whitespace(self):
-    ws_len = re.match('\s*', self.content).end()
-    self.__strip(ws_len)
-    return
-
-  def __strip(self, length):
-    """
-    Remove a given amount of chars from the input and update
-    the offset.
-    """
-    self.content = self.content[length:]
-    self.offset += length
-  
-  def evaluate(self, context):
-    """
-    Evaluate the expression with the given context
-    """
-    
-    # Helper function to evaluate __get_equality results
-    def eval_equality(tok):
-      left = opmap[tok[0].type](tok[0])
-      right = opmap[tok[2].type](tok[2])
-      rv = left == right
-      if tok[1].value == '!=':
-        rv = not rv
-      return rv
-    # Helper function to evaluate __get_logical_and and __get_logical_or results
-    def eval_logical_op(tok):
-      left = opmap[tok[0].type](tok[0])
-      right = opmap[tok[2].type](tok[2])
-      if tok[1].value == '&&':
-        return left and right
-      elif tok[1].value == '||':
-        return left or right
-      raise Expression.ParseError, self
-
-    # Mapping from token types to evaluator functions
-    # Apart from (non-)equality, all these can be simple lambda forms.
-    opmap = {
-      'logical_op': eval_logical_op,
-      'equality': eval_equality,
-      'not': lambda tok: not opmap[tok[0].type](tok[0]),
-      'string': lambda tok: context[tok.value],
-      'defined': lambda tok: tok.value in context,
-      'int': lambda tok: tok.value}
-
-    return opmap[self.e.type](self.e);
-  
-  class __AST(list):
-    """
-    Internal class implementing Abstract Syntax Tree nodes
-    """
-    def __init__(self, type):
-      self.type = type
-      super(self.__class__, self).__init__(self)
-  
-  class __ASTLeaf:
-    """
-    Internal class implementing Abstract Syntax Tree leafs
-    """
-    def __init__(self, type, value):
-      self.value = value
-      self.type = type
-    def __str__(self):
-      return self.value.__str__()
-    def __repr__(self):
-      return self.value.__repr__()
-  
-  class ParseError(StandardError):
-    """
-    Error raised when parsing fails.
-    It has two members, offset and content, which give the offset of the
-    error and the offending content.
-    """
-    def __init__(self, expression):
-      self.offset = expression.offset
-      self.content = expression.content[:3]
-    def __str__(self):
-      return 'Unexpected content at offset {0}, "{1}"'.format(self.offset, 
-                                                              self.content)
-
-class Context(dict):
-  """
-  This class holds variable values by subclassing dict, and while it
-  truthfully reports True and False on
-  
-  name in context
-  
-  it returns the variable name itself on
-  
-  context["name"]
-
-  to reflect the ambiguity between string literals and preprocessor
-  variables.
-  """
-  def __getitem__(self, key):
-    if key in self:
-      return super(self.__class__, self).__getitem__(key)
-    return key
--- a/config/Makefile.in
+++ b/config/Makefile.in
@@ -69,17 +69,17 @@ else
 endif
 
 ifdef WRAP_SYSTEM_INCLUDES
 export-preqs = \
   $(call mkdir_deps,system_wrappers) \
   $(NULL)
 
 export:: $(export-preqs)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) \
+	$(PYTHON) -m mozbuild.action.preprocessor $(DEFINES) $(ACDEFINES) \
 		-DMOZ_TREE_CAIRO=$(MOZ_TREE_CAIRO) \
 		-DMOZ_TREE_PIXMAN=$(MOZ_TREE_PIXMAN) \
 		-DMOZ_NATIVE_HUNSPELL=$(MOZ_NATIVE_HUNSPELL) \
 		-DMOZ_NATIVE_BZ2=$(MOZ_NATIVE_BZ2) \
 		-DMOZ_NATIVE_ZLIB=$(MOZ_NATIVE_ZLIB) \
 		-DMOZ_NATIVE_PNG=$(MOZ_NATIVE_PNG) \
 		-DMOZ_NATIVE_JPEG=$(MOZ_NATIVE_JPEG) \
 		-DMOZ_NATIVE_LIBEVENT=$(MOZ_NATIVE_LIBEVENT) \
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -1554,26 +1554,26 @@ install_targets_sanity = $(if $(filter-o
 #   FOO := input-file
 #   FOO_PATH := target-directory
 #   FOO_FLAGS := -Dsome_flag
 #   PP_TARGETS += FOO
 #
 # If PP_TARGETS lists a category name <C> (like FOO, above), then we consult the
 # following make variables to see what to do:
 #
-# - <C> lists input files to be preprocessed with config/Preprocessor.py. We
-#   search VPATH for the names given here. If an input file name ends in '.in',
-#   that suffix is omitted from the output file name.
+# - <C> lists input files to be preprocessed with mozbuild.action.preprocessor.
+#   We search VPATH for the names given here. If an input file name ends in
+#   '.in', that suffix is omitted from the output file name.
 #
 # - <C>_PATH names the directory in which to place the preprocessed output
 #   files. We create this directory if it does not already exist. Setting
 #   this variable is optional; if unset, we install the files in $(CURDIR).
 #
-# - <C>_FLAGS lists flags to pass to Preprocessor.py, in addition to the usual
-#   bunch. Setting this variable is optional.
+# - <C>_FLAGS lists flags to pass to mozbuild.action.preprocessor, in addition
+#   to the usual bunch. Setting this variable is optional.
 #
 # - <C>_TARGET names the 'make' target that should depend on creating the output
 #   files. Setting this variable is optional; if unset, we preprocess the
 #   files for the 'libs' target.
 #
 # - <C>_KEEP_PATH may be set to 1 to indicate the paths given in <C> are to be
 #   kept under <C>_PATH. That is, if <C> is bar/baz/qux.h.in and <C>_PATH is
 #   $(DIST)/include, the preprocessed file would be $(DIST)/include/bar/baz/qux.h
@@ -1599,17 +1599,17 @@ pp_target_results = $(foreach file,$($(1
 $(foreach tier,$(PP_TARGETS_TIERS), \
   $(eval $(tier):: $(PP_TARGETS_RESULTS_$(tier))) \
 )
 
 $(sort $(foreach tier,$(PP_TARGETS_TIERS),$(PP_TARGETS_RESULTS_$(tier)))):
 	$(if $(filter-out $(notdir $@),$(notdir $(<:.in=))),$(error Looks like $@ has an unexpected dependency on $< which breaks PP_TARGETS))
 	$(REPORT_BUILD)
 	$(RM) "$@"
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(PP_TARGET_FLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) "$<" -o "$@"
+	$(call py_action,preprocessor,$(PP_TARGET_FLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) "$<" -o "$@")
 
 
 # Pull in non-recursive targets if this is a partial tree build.
 ifndef TOPLEVEL_BUILD
 include $(topsrcdir)/config/makefiles/nonrecursive.mk
 endif
 
 ################################################################################
--- a/db/sqlite3/src/Makefile.in
+++ b/db/sqlite3/src/Makefile.in
@@ -21,18 +21,18 @@ GARBAGE += \
 
 # We generate the appropriate version header file with our python script.
 sqlite-version.h: sqlite-version.py sqlite3.h
 	$(PYTHON) $^ > $@
 
 # We have to preprocess our def file because we need different symbols in debug
 # builds exposed that are not built in non-debug builds.
 $(DEFFILE): sqlite.def
-	@$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) \
-	  $(srcdir)/sqlite.def > $(DEFFILE)
+	@$(call py_action,preprocessor,$(DEFINES) \
+	  $(srcdir)/sqlite.def -o $(DEFFILE))
 
 export:: sqlite-version.h
 endif
 
 ifeq (Darwin,$(OS_TARGET))
 # On OSX, with jemalloc enabled, having sqlite linked against mozglue
 # causes crashes in NSS standalone tools.
 MOZ_GLUE_LDFLAGS =
@@ -113,12 +113,12 @@ endif
 endif
 
 include $(topsrcdir)/config/rules.mk
 
 # next line allows use of MOZ_OBJDIR in .mozconfig with older gcc on BeOS, maybe others
 LOCAL_INCLUDES += -I$(srcdir)
 
 ifeq ($(OS_ARCH),OS2)
-ADD_TO_DEF_FILE = $(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) \
+ADD_TO_DEF_FILE = $(PYTHON) -m mozbuild.action.preprocessor $(DEFINES) \
        $(srcdir)/sqlite.def | sed -e '1,/^EXPORTS$$/ d' -e 's,sqlite3,_\0,' \
        -e 's,\ DATA.*$$,,' >> $(DEF_FILE)
 endif
--- a/dom/bindings/Makefile.in
+++ b/dom/bindings/Makefile.in
@@ -152,24 +152,24 @@ CSS2Properties.webidl: $(topsrcdir)/layo
 # We can't easily use PP_TARGETS here because it insists on outputting targets
 # that look like "$(CURDIR)/foo" whereas we want our target to just be "foo".
 # Make sure to include $(GLOBAL_DEPS) so we pick up changes to what symbols are
 # defined.  Also make sure to remove $@ before writing to it, because otherwise
 # if a file goes from non-preprocessed to preprocessed we can end up writing to
 # a symlink, which will clobber files in the srcdir, which is bad.
 $(preprocessed_webidl_files): %: $(webidl_base)/% $(GLOBAL_DEPS)
 	$(RM) $@
-	PYTHONDONTWRITEBYTECODE=1 $(PYTHON) \
-          $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(webidl_base)/$* -o $@
+	$(call py_action,preprocessor, \
+          $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(webidl_base)/$* -o $@)
 
 # See the comment about PP_TARGETS for $(preprocessed_webidl_files)
 $(preprocessed_test_webidl_files): %: $(srcdir)/test/% $(GLOBAL_DEPS)
 	$(RM) $@
-	PYTHONDONTWRITEBYTECODE=1 $(PYTHON) \
-          $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(srcdir)/test/$* -o $@
+	$(call py_action,preprocessor, \
+          $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(srcdir)/test/$* -o $@)
 
 # Make is dumb and can get confused between "foo" and "$(CURDIR)/foo".  Make
 # sure that the latter depends on the former, since the latter gets used in .pp
 # files.
 all_webidl_files_absolute = $(addprefix $(CURDIR)/,$(all_webidl_files))
 $(all_webidl_files_absolute): $(CURDIR)/%: %
 
 $(generated_header_files): .BindingGen
--- a/js/src/Makefile.in
+++ b/js/src/Makefile.in
@@ -174,18 +174,18 @@ config/nsinstall$(HOST_BIN_SUFFIX): $(sr
 endif
 
 # Ensure symbol versions of shared library on Linux do not conflict
 # with those in libxul.
 ifeq (Linux,$(OS_TARGET))
 EXTRA_DSO_LDOPTS += -Wl,-version-script,symverscript
 
 symverscript: symverscript.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-		-DVERSION="$(subst -,_,$(LIBRARY_NAME))" $< > $@
+	$(call py_action,preprocessor, \
+		-DVERSION="$(subst -,_,$(LIBRARY_NAME))" $< -o $@)
 
 EXTRA_DEPS += symverscript
 endif
 
 export_files = js-config.h
 ifdef HAVE_DTRACE
 export_files += $(CURDIR)/javascript-trace.h
 endif
@@ -521,24 +521,24 @@ JS_CONFIG_SUBSTITUTIONS=\
 	-DMOZJS_PATCH_VERSION="$(MOZJS_PATCH_VERSION)" \
 	-DMOZJS_ALPHA="$(MOZJS_ALPHA)" \
 	-DNSPR_CFLAGS="$(NSPR_CFLAGS)" \
 	-DNSPR_PKGCONF_CHECK="$(NSPR_PKGCONF_CHECK)" \
 	$(NULL)
 
 $(JS_CONFIG_NAME): js-config.in Makefile $(DEPTH)/config/autoconf.mk $(topsrcdir)/config/config.mk $(topsrcdir)/config/rules.mk
 	$(RM) $@.tmp
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py --marker % $(JS_CONFIG_SUBSTITUTIONS) $< > $@.tmp \
-	&& mv $@.tmp $@ && chmod +x $@
+	$(call py_action,preprocessor,--marker % $(JS_CONFIG_SUBSTITUTIONS) $< -o $@.tmp)
+	mv $@.tmp $@ && chmod +x $@
 
 SCRIPTS = $(JS_CONFIG_NAME)
 SDK_BINARY = $(JS_CONFIG_NAME)
 
 $(LIBRARY_NAME).pc: js.pc.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(JS_CONFIG_SUBSTITUTIONS) $< > $@
+	$(call py_action,preprocessor,$(JS_CONFIG_SUBSTITUTIONS) $< -o $@)
 
 install:: $(LIBRARY_NAME).pc
 	$(SYSINSTALL) $^ $(DESTDIR)$(libdir)/pkgconfig
 
 ######################################################
 # BEGIN SpiderMonkey header installation
 #
 # Mozilla/Gecko/Firefox mostly doesn't concern itself with defining a sensible
deleted file mode 100644
--- a/js/src/config/Expression.py
+++ /dev/null
@@ -1,241 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-"""
-Parses and evaluates simple statements for Preprocessor:
-
-Expression currently supports the following grammar, whitespace is ignored:
-
-expression :
-  and_cond ( '||' expression ) ? ;
-and_cond:
-  test ( '&&' and_cond ) ? ;
-test:
-  unary ( ( '==' | '!=' ) unary ) ? ;
-unary :
-  '!'? value ;
-value :
-  [0-9]+ # integer
-  | 'defined(' \w+ ')'
-  | \w+  # string identifier or value;
-"""
-
-import re
-
-class Expression:
-  def __init__(self, expression_string):
-    """
-    Create a new expression with this string.
-    The expression will already be parsed into an Abstract Syntax Tree.
-    """
-    self.content = expression_string
-    self.offset = 0
-    self.__ignore_whitespace()
-    self.e = self.__get_logical_or()
-    if self.content:
-      raise Expression.ParseError, self
-
-  def __get_logical_or(self):
-    """
-    Production: and_cond ( '||' expression ) ?
-    """
-    if not len(self.content):
-      return None
-    rv = Expression.__AST("logical_op")
-    # test
-    rv.append(self.__get_logical_and())
-    self.__ignore_whitespace()
-    if self.content[:2] != '||':
-      # no logical op needed, short cut to our prime element
-      return rv[0]
-    # append operator
-    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
-    self.__strip(2)
-    self.__ignore_whitespace()
-    rv.append(self.__get_logical_or())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_logical_and(self):
-    """
-    Production: test ( '&&' and_cond ) ?
-    """
-    if not len(self.content):
-      return None
-    rv = Expression.__AST("logical_op")
-    # test
-    rv.append(self.__get_equality())
-    self.__ignore_whitespace()
-    if self.content[:2] != '&&':
-      # no logical op needed, short cut to our prime element
-      return rv[0]
-    # append operator
-    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
-    self.__strip(2)
-    self.__ignore_whitespace()
-    rv.append(self.__get_logical_and())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_equality(self):
-    """
-    Production: unary ( ( '==' | '!=' ) unary ) ?
-    """
-    if not len(self.content):
-      return None
-    rv = Expression.__AST("equality")
-    # unary 
-    rv.append(self.__get_unary())
-    self.__ignore_whitespace()
-    if not re.match('[=!]=', self.content):
-      # no equality needed, short cut to our prime unary
-      return rv[0]
-    # append operator
-    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
-    self.__strip(2)
-    self.__ignore_whitespace()
-    rv.append(self.__get_unary())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_unary(self):
-    """
-    Production: '!'? value
-    """
-    # eat whitespace right away, too
-    not_ws = re.match('!\s*', self.content)
-    if not not_ws:
-      return self.__get_value()
-    rv = Expression.__AST('not')
-    self.__strip(not_ws.end())
-    rv.append(self.__get_value())
-    self.__ignore_whitespace()
-    return rv
-
-  def __get_value(self):
-    """
-    Production: ( [0-9]+ | 'defined(' \w+ ')' | \w+ )
-    Note that the order is important, and the expression is kind-of
-    ambiguous as \w includes 0-9. One could make it unambiguous by
-    removing 0-9 from the first char of a string literal.
-    """
-    rv = None
-    m = re.match('defined\s*\(\s*(\w+)\s*\)', self.content)
-    if m:
-      word_len = m.end()
-      rv = Expression.__ASTLeaf('defined', m.group(1))
-    else:
-      word_len = re.match('[0-9]*', self.content).end()
-      if word_len:
-        value = int(self.content[:word_len])
-        rv = Expression.__ASTLeaf('int', value)
-      else:
-        word_len = re.match('\w*', self.content).end()
-        if word_len:
-          rv = Expression.__ASTLeaf('string', self.content[:word_len])
-        else:
-          raise Expression.ParseError, self
-    self.__strip(word_len)
-    self.__ignore_whitespace()
-    return rv
-
-  def __ignore_whitespace(self):
-    ws_len = re.match('\s*', self.content).end()
-    self.__strip(ws_len)
-    return
-
-  def __strip(self, length):
-    """
-    Remove a given amount of chars from the input and update
-    the offset.
-    """
-    self.content = self.content[length:]
-    self.offset += length
-  
-  def evaluate(self, context):
-    """
-    Evaluate the expression with the given context
-    """
-    
-    # Helper function to evaluate __get_equality results
-    def eval_equality(tok):
-      left = opmap[tok[0].type](tok[0])
-      right = opmap[tok[2].type](tok[2])
-      rv = left == right
-      if tok[1].value == '!=':
-        rv = not rv
-      return rv
-    # Helper function to evaluate __get_logical_and and __get_logical_or results
-    def eval_logical_op(tok):
-      left = opmap[tok[0].type](tok[0])
-      right = opmap[tok[2].type](tok[2])
-      if tok[1].value == '&&':
-        return left and right
-      elif tok[1].value == '||':
-        return left or right
-      raise Expression.ParseError, self
-
-    # Mapping from token types to evaluator functions
-    # Apart from (non-)equality, all these can be simple lambda forms.
-    opmap = {
-      'logical_op': eval_logical_op,
-      'equality': eval_equality,
-      'not': lambda tok: not opmap[tok[0].type](tok[0]),
-      'string': lambda tok: context[tok.value],
-      'defined': lambda tok: tok.value in context,
-      'int': lambda tok: tok.value}
-
-    return opmap[self.e.type](self.e);
-  
-  class __AST(list):
-    """
-    Internal class implementing Abstract Syntax Tree nodes
-    """
-    def __init__(self, type):
-      self.type = type
-      super(self.__class__, self).__init__(self)
-  
-  class __ASTLeaf:
-    """
-    Internal class implementing Abstract Syntax Tree leafs
-    """
-    def __init__(self, type, value):
-      self.value = value
-      self.type = type
-    def __str__(self):
-      return self.value.__str__()
-    def __repr__(self):
-      return self.value.__repr__()
-  
-  class ParseError(StandardError):
-    """
-    Error raised when parsing fails.
-    It has two members, offset and content, which give the offset of the
-    error and the offending content.
-    """
-    def __init__(self, expression):
-      self.offset = expression.offset
-      self.content = expression.content[:3]
-    def __str__(self):
-      return 'Unexpected content at offset {0}, "{1}"'.format(self.offset, 
-                                                              self.content)
-
-class Context(dict):
-  """
-  This class holds variable values by subclassing dict, and while it
-  truthfully reports True and False on
-  
-  name in context
-  
-  it returns the variable name itself on
-  
-  context["name"]
-
-  to reflect the ambiguity between string literals and preprocessor
-  variables.
-  """
-  def __getitem__(self, key):
-    if key in self:
-      return super(self.__class__, self).__getitem__(key)
-    return key
--- a/js/src/config/Makefile.in
+++ b/js/src/config/Makefile.in
@@ -35,17 +35,17 @@ endif
 include $(topsrcdir)/config/rules.mk
 
 HOST_CFLAGS += -DUNICODE -D_UNICODE
 
 ifdef WRAP_SYSTEM_INCLUDES
 export:: \
   $(call mkdir_deps,system_wrappers_js) \
   $(NULL)
-	$(PYTHON) $(srcdir)/Preprocessor.py $(DEFINES) $(ACDEFINES) \
+	$(PYTHON) -m mozbuild.action.preprocessor $(DEFINES) $(ACDEFINES) \
 		-DMOZ_NATIVE_ICU=$(MOZ_NATIVE_ICU) \
 		$(srcdir)/system-headers | $(PERL) $(srcdir)/make-system-wrappers.pl system_wrappers_js
 	$(INSTALL) system_wrappers_js $(DIST)
 
 GARBAGE_DIRS += system_wrappers_js
 endif
 
 GARBAGE += $(srcdir)/*.pyc *.pyc
deleted file mode 100755
--- a/js/src/config/Preprocessor.py
+++ /dev/null
@@ -1,492 +0,0 @@
-"""
-This is a very primitive line based preprocessor, for times when using
-a C preprocessor isn't an option.
-"""
-
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import sys
-import os
-import os.path
-import re
-from optparse import OptionParser
-import errno
-
-# hack around win32 mangling our line endings
-# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65443
-if sys.platform == "win32":
-  import msvcrt
-  msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
-  os.linesep = '\n'
-
-import Expression
-
-__all__ = ['Preprocessor', 'preprocess']
-
-
-class Preprocessor:
-  """
-  Class for preprocessing text files.
-  """
-  class Error(RuntimeError):
-    def __init__(self, cpp, MSG, context):
-      self.file = cpp.context['FILE']
-      self.line = cpp.context['LINE']
-      self.key = MSG
-      RuntimeError.__init__(self, (self.file, self.line, self.key, context))
-  def __init__(self):
-    self.context = Expression.Context()
-    for k,v in {'FILE': '',
-                'LINE': 0,
-                'DIRECTORY': os.path.abspath('.')}.iteritems():
-      self.context[k] = v
-    self.actionLevel = 0
-    self.disableLevel = 0
-    # ifStates can be
-    #  0: hadTrue
-    #  1: wantsTrue
-    #  2: #else found
-    self.ifStates = []
-    self.checkLineNumbers = False
-    self.writtenLines = 0
-    self.filters = []
-    self.cmds = {}
-    for cmd, level in {'define': 0,
-                       'undef': 0,
-                       'if': sys.maxint,
-                       'ifdef': sys.maxint,
-                       'ifndef': sys.maxint,
-                       'else': 1,
-                       'elif': 1,
-                       'elifdef': 1,
-                       'elifndef': 1,
-                       'endif': sys.maxint,
-                       'expand': 0,
-                       'literal': 0,
-                       'filter': 0,
-                       'unfilter': 0,
-                       'include': 0,
-                       'includesubst': 0,
-                       'error': 0}.iteritems():
-      self.cmds[cmd] = (level, getattr(self, 'do_' + cmd))
-    self.out = sys.stdout
-    self.setMarker('#')
-    self.LE = '\n'
-    self.varsubst = re.compile('@(?P<VAR>\w+)@', re.U)
-  
-  def warnUnused(self, file):
-    if self.actionLevel == 0:
-      sys.stderr.write('{0}: WARNING: no preprocessor directives found\n'.format(file))
-    elif self.actionLevel == 1:
-      sys.stderr.write('{0}: WARNING: no useful preprocessor directives found\n'.format(file))
-    pass
-
-  def setLineEndings(self, aLE):
-    """
-    Set the line endings to be used for output.
-    """
-    self.LE = {'cr': '\x0D', 'lf': '\x0A', 'crlf': '\x0D\x0A'}[aLE]
-  
-  def setMarker(self, aMarker):
-    """
-    Set the marker to be used for processing directives.
-    Used for handling CSS files, with pp.setMarker('%'), for example.
-    The given marker may be None, in which case no markers are processed.
-    """
-    self.marker = aMarker
-    if aMarker:
-      self.instruction = re.compile('{0}(?P<cmd>[a-z]+)(?:\s(?P<args>.*))?$'
-                                    .format(aMarker), 
-                                    re.U)
-      self.comment = re.compile(aMarker, re.U)
-    else:
-      class NoMatch(object):
-        def match(self, *args):
-          return False
-      self.instruction = self.comment = NoMatch()
-  
-  def clone(self):
-    """
-    Create a clone of the current processor, including line ending
-    settings, marker, variable definitions, output stream.
-    """
-    rv = Preprocessor()
-    rv.context.update(self.context)
-    rv.setMarker(self.marker)
-    rv.LE = self.LE
-    rv.out = self.out
-    return rv
-  
-  def applyFilters(self, aLine):
-    for f in self.filters:
-      aLine = f[1](aLine)
-    return aLine
-  
-  def write(self, aLine):
-    """
-    Internal method for handling output.
-    """
-    if self.checkLineNumbers:
-      self.writtenLines += 1
-      ln = self.context['LINE']
-      if self.writtenLines != ln:
-        self.out.write('//@line {line} "{file}"{le}'.format(line=ln,
-                                                            file=self.context['FILE'],
-                                                            le=self.LE))
-        self.writtenLines = ln
-    filteredLine = self.applyFilters(aLine)
-    if filteredLine != aLine:
-      self.actionLevel = 2
-    # ensure our line ending. Only need to handle \n, as we're reading
-    # with universal line ending support, at least for files.
-    filteredLine = re.sub('\n', self.LE, filteredLine)
-    self.out.write(filteredLine)
-  
-  def handleCommandLine(self, args, defaultToStdin = False):
-    """
-    Parse a commandline into this parser.
-    Uses OptionParser internally, no args mean sys.argv[1:].
-    """
-    p = self.getCommandLineParser()
-    (options, args) = p.parse_args(args=args)
-    includes = options.I
-    if options.output:
-      dir = os.path.dirname(options.output)
-      if dir and not os.path.exists(dir):
-        try:
-          os.makedirs(dir)
-        except OSError as error:
-          if error.errno != errno.EEXIST:
-            raise
-      self.out = open(options.output, 'wb')
-    if defaultToStdin and len(args) == 0:
-      args = [sys.stdin]
-    includes.extend(args)
-    if includes:
-      for f in includes:
-        self.do_include(f, False)
-      self.warnUnused(f)
-    pass
-
-  def getCommandLineParser(self, unescapeDefines = False):
-    escapedValue = re.compile('".*"$')
-    numberValue = re.compile('\d+$')
-    def handleE(option, opt, value, parser):
-      for k,v in os.environ.iteritems():
-        self.context[k] = v
-    def handleD(option, opt, value, parser):
-      vals = value.split('=', 1)
-      if len(vals) == 1:
-        vals.append(1)
-      elif unescapeDefines and escapedValue.match(vals[1]):
-        # strip escaped string values
-        vals[1] = vals[1][1:-1]
-      elif numberValue.match(vals[1]):
-        vals[1] = int(vals[1])
-      self.context[vals[0]] = vals[1]
-    def handleU(option, opt, value, parser):
-      del self.context[value]
-    def handleF(option, opt, value, parser):
-      self.do_filter(value)
-    def handleLE(option, opt, value, parser):
-      self.setLineEndings(value)
-    def handleMarker(option, opt, value, parser):
-      self.setMarker(value)
-    p = OptionParser()
-    p.add_option('-I', action='append', type="string", default = [],
-                 metavar="FILENAME", help='Include file')
-    p.add_option('-E', action='callback', callback=handleE,
-                 help='Import the environment into the defined variables')
-    p.add_option('-D', action='callback', callback=handleD, type="string",
-                 metavar="VAR[=VAL]", help='Define a variable')
-    p.add_option('-U', action='callback', callback=handleU, type="string",
-                 metavar="VAR", help='Undefine a variable')
-    p.add_option('-F', action='callback', callback=handleF, type="string",
-                 metavar="FILTER", help='Enable the specified filter')
-    p.add_option('-o', '--output', type="string", default=None,
-                 metavar="FILENAME", help='Output to the specified file '+
-                 'instead of stdout')
-    p.add_option('--line-endings', action='callback', callback=handleLE,
-                 type="string", metavar="[cr|lr|crlf]",
-                 help='Use the specified line endings [Default: OS dependent]')
-    p.add_option('--marker', action='callback', callback=handleMarker,
-                 type="string",
-                 help='Use the specified marker instead of #')
-    return p
-
-  def handleLine(self, aLine):
-    """
-    Handle a single line of input (internal).
-    """
-    if self.actionLevel == 0 and self.comment.match(aLine):
-      self.actionLevel = 1
-    m = self.instruction.match(aLine)
-    if m:
-      args = None
-      cmd = m.group('cmd')
-      try:
-        args = m.group('args')
-      except IndexError:
-        pass
-      if cmd not in self.cmds:
-        raise Preprocessor.Error(self, 'INVALID_CMD', aLine)
-      level, cmd = self.cmds[cmd]
-      if (level >= self.disableLevel):
-        cmd(args)
-      if cmd != 'literal':
-        self.actionLevel = 2
-    elif self.disableLevel == 0 and not self.comment.match(aLine):
-      self.write(aLine)
-    pass
-
-  # Instruction handlers
-  # These are named do_'instruction name' and take one argument
-  
-  # Variables
-  def do_define(self, args):
-    m = re.match('(?P<name>\w+)(?:\s(?P<value>.*))?', args, re.U)
-    if not m:
-      raise Preprocessor.Error(self, 'SYNTAX_DEF', args)
-    val = 1
-    if m.group('value'):
-      val = self.applyFilters(m.group('value'))
-      try:
-        val = int(val)
-      except:
-        pass
-    self.context[m.group('name')] = val
-  def do_undef(self, args):
-    m = re.match('(?P<name>\w+)$', args, re.U)
-    if not m:
-      raise Preprocessor.Error(self, 'SYNTAX_DEF', args)
-    if args in self.context:
-      del self.context[args]
-  # Logic
-  def ensure_not_else(self):
-    if len(self.ifStates) == 0 or self.ifStates[-1] == 2:
-      sys.stderr.write('WARNING: bad nesting of #else\n')
-  def do_if(self, args, replace=False):
-    if self.disableLevel and not replace:
-      self.disableLevel += 1
-      return
-    val = None
-    try:
-      e = Expression.Expression(args)
-      val = e.evaluate(self.context)
-    except Exception:
-      # XXX do real error reporting
-      raise Preprocessor.Error(self, 'SYNTAX_ERR', args)
-    if type(val) == str:
-      # we're looking for a number value, strings are false
-      val = False
-    if not val:
-      self.disableLevel = 1
-    if replace:
-      if val:
-        self.disableLevel = 0
-      self.ifStates[-1] = self.disableLevel
-    else:
-      self.ifStates.append(self.disableLevel)
-    pass
-  def do_ifdef(self, args, replace=False):
-    if self.disableLevel and not replace:
-      self.disableLevel += 1
-      return
-    if re.match('\W', args, re.U):
-      raise Preprocessor.Error(self, 'INVALID_VAR', args)
-    if args not in self.context:
-      self.disableLevel = 1
-    if replace:
-      if args in self.context:
-        self.disableLevel = 0
-      self.ifStates[-1] = self.disableLevel
-    else:
-      self.ifStates.append(self.disableLevel)
-    pass
-  def do_ifndef(self, args, replace=False):
-    if self.disableLevel and not replace:
-      self.disableLevel += 1
-      return
-    if re.match('\W', args, re.U):
-      raise Preprocessor.Error(self, 'INVALID_VAR', args)
-    if args in self.context:
-      self.disableLevel = 1
-    if replace:
-      if args not in self.context:
-        self.disableLevel = 0
-      self.ifStates[-1] = self.disableLevel
-    else:
-      self.ifStates.append(self.disableLevel)
-    pass
-  def do_else(self, args, ifState = 2):
-    self.ensure_not_else()
-    hadTrue = self.ifStates[-1] == 0
-    self.ifStates[-1] = ifState # in-else
-    if hadTrue:
-      self.disableLevel = 1
-      return
-    self.disableLevel = 0
-  def do_elif(self, args):
-    if self.disableLevel == 1:
-      if self.ifStates[-1] == 1:
-        self.do_if(args, replace=True)
-    else:
-      self.do_else(None, self.ifStates[-1])
-  def do_elifdef(self, args):
-    if self.disableLevel == 1:
-      if self.ifStates[-1] == 1:
-        self.do_ifdef(args, replace=True)
-    else:
-      self.do_else(None, self.ifStates[-1])
-  def do_elifndef(self, args):
-    if self.disableLevel == 1:
-      if self.ifStates[-1] == 1:
-        self.do_ifndef(args, replace=True)
-    else:
-      self.do_else(None, self.ifStates[-1])
-  def do_endif(self, args):
-    if self.disableLevel > 0:
-      self.disableLevel -= 1
-    if self.disableLevel == 0:
-      self.ifStates.pop()
-  # output processing
-  def do_expand(self, args):
-    lst = re.split('__(\w+)__', args, re.U)
-    do_replace = False
-    def vsubst(v):
-      if v in self.context:
-        return str(self.context[v])
-      return ''
-    for i in range(1, len(lst), 2):
-      lst[i] = vsubst(lst[i])
-    lst.append('\n') # add back the newline
-    self.write(reduce(lambda x, y: x+y, lst, ''))
-  def do_literal(self, args):
-    self.write(args + self.LE)
-  def do_filter(self, args):
-    filters = [f for f in args.split(' ') if hasattr(self, 'filter_' + f)]
-    if len(filters) == 0:
-      return
-    current = dict(self.filters)
-    for f in filters:
-      current[f] = getattr(self, 'filter_' + f)
-    filterNames = current.keys()
-    filterNames.sort()
-    self.filters = [(fn, current[fn]) for fn in filterNames]
-    return
-  def do_unfilter(self, args):
-    filters = args.split(' ')
-    current = dict(self.filters)
-    for f in filters:
-      if f in current:
-        del current[f]
-    filterNames = current.keys()
-    filterNames.sort()
-    self.filters = [(fn, current[fn]) for fn in filterNames]
-    return
-  # Filters
-  #
-  # emptyLines
-  #   Strips blank lines from the output.
-  def filter_emptyLines(self, aLine):
-    if aLine == '\n':
-      return ''
-    return aLine
-  # slashslash
-  #   Strips everything after //
-  def filter_slashslash(self, aLine):
-    if (aLine.find('//') == -1):
-      return aLine
-    [aLine, rest] = aLine.split('//', 1)
-    if rest:
-      aLine += '\n'
-    return aLine
-  # spaces
-  #   Collapses sequences of spaces into a single space
-  def filter_spaces(self, aLine):
-    return re.sub(' +', ' ', aLine).strip(' ')
-  # substition
-  #   helper to be used by both substition and attemptSubstitution
-  def filter_substitution(self, aLine, fatal=True):
-    def repl(matchobj):
-      varname = matchobj.group('VAR')
-      if varname in self.context:
-        return str(self.context[varname])
-      if fatal:
-        raise Preprocessor.Error(self, 'UNDEFINED_VAR', varname)
-      return matchobj.group(0)
-    return self.varsubst.sub(repl, aLine)
-  def filter_attemptSubstitution(self, aLine):
-    return self.filter_substitution(aLine, fatal=False)
-  # File ops
-  def do_include(self, args, filters=True):
-    """
-    Preprocess a given file.
-    args can either be a file name, or a file-like object.
-    Files should be opened, and will be closed after processing.
-    """
-    isName = type(args) == str or type(args) == unicode
-    oldWrittenLines = self.writtenLines
-    oldCheckLineNumbers = self.checkLineNumbers
-    self.checkLineNumbers = False
-    if isName:
-      try:
-        args = str(args)
-        if filters:
-          args = self.applyFilters(args)
-        if not os.path.isabs(args):
-          args = os.path.join(self.context['DIRECTORY'], args)
-        args = open(args, 'rU')
-      except Preprocessor.Error:
-        raise
-      except:
-        raise Preprocessor.Error(self, 'FILE_NOT_FOUND', str(args))
-    self.checkLineNumbers = bool(re.search('\.(js|jsm|java)(?:\.in)?$', args.name))
-    oldFile = self.context['FILE']
-    oldLine = self.context['LINE']
-    oldDir = self.context['DIRECTORY']
-    if args.isatty():
-      # we're stdin, use '-' and '' for file and dir
-      self.context['FILE'] = '-'
-      self.context['DIRECTORY'] = ''
-    else:
-      abspath = os.path.abspath(args.name)
-      self.context['FILE'] = abspath
-      self.context['DIRECTORY'] = os.path.dirname(abspath)
-    self.context['LINE'] = 0
-    self.writtenLines = 0
-    for l in args:
-      self.context['LINE'] += 1
-      self.handleLine(l)
-    args.close()
-    self.context['FILE'] = oldFile
-    self.checkLineNumbers = oldCheckLineNumbers
-    self.writtenLines = oldWrittenLines
-    self.context['LINE'] = oldLine
-    self.context['DIRECTORY'] = oldDir
-  def do_includesubst(self, args):
-    args = self.filter_substitution(args)
-    self.do_include(args)
-  def do_error(self, args):
-    raise Preprocessor.Error(self, 'Error: ', str(args))
-
-def main():
-  pp = Preprocessor()
-  pp.handleCommandLine(None, True)
-  return
-
-def preprocess(includes=[sys.stdin], defines={},
-               output = sys.stdout,
-               line_endings='\n', marker='#'):
-  pp = Preprocessor()
-  pp.context.update(defines)
-  pp.setLineEndings(line_endings)
-  pp.setMarker(marker)
-  pp.out = output
-  for f in includes:
-    pp.do_include(f, False)
-
-if __name__ == "__main__":
-  main()
--- a/js/src/config/rules.mk
+++ b/js/src/config/rules.mk
@@ -1554,26 +1554,26 @@ install_targets_sanity = $(if $(filter-o
 #   FOO := input-file
 #   FOO_PATH := target-directory
 #   FOO_FLAGS := -Dsome_flag
 #   PP_TARGETS += FOO
 #
 # If PP_TARGETS lists a category name <C> (like FOO, above), then we consult the
 # following make variables to see what to do:
 #
-# - <C> lists input files to be preprocessed with config/Preprocessor.py. We
-#   search VPATH for the names given here. If an input file name ends in '.in',
-#   that suffix is omitted from the output file name.
+# - <C> lists input files to be preprocessed with mozbuild.action.preprocessor.
+#   We search VPATH for the names given here. If an input file name ends in
+#   '.in', that suffix is omitted from the output file name.
 #
 # - <C>_PATH names the directory in which to place the preprocessed output
 #   files. We create this directory if it does not already exist. Setting
 #   this variable is optional; if unset, we install the files in $(CURDIR).
 #
-# - <C>_FLAGS lists flags to pass to Preprocessor.py, in addition to the usual
-#   bunch. Setting this variable is optional.
+# - <C>_FLAGS lists flags to pass to mozbuild.action.preprocessor, in addition
+#   to the usual bunch. Setting this variable is optional.
 #
 # - <C>_TARGET names the 'make' target that should depend on creating the output
 #   files. Setting this variable is optional; if unset, we preprocess the
 #   files for the 'libs' target.
 #
 # - <C>_KEEP_PATH may be set to 1 to indicate the paths given in <C> are to be
 #   kept under <C>_PATH. That is, if <C> is bar/baz/qux.h.in and <C>_PATH is
 #   $(DIST)/include, the preprocessed file would be $(DIST)/include/bar/baz/qux.h
@@ -1599,17 +1599,17 @@ pp_target_results = $(foreach file,$($(1
 $(foreach tier,$(PP_TARGETS_TIERS), \
   $(eval $(tier):: $(PP_TARGETS_RESULTS_$(tier))) \
 )
 
 $(sort $(foreach tier,$(PP_TARGETS_TIERS),$(PP_TARGETS_RESULTS_$(tier)))):
 	$(if $(filter-out $(notdir $@),$(notdir $(<:.in=))),$(error Looks like $@ has an unexpected dependency on $< which breaks PP_TARGETS))
 	$(REPORT_BUILD)
 	$(RM) "$@"
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(PP_TARGET_FLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) "$<" -o "$@"
+	$(call py_action,preprocessor,$(PP_TARGET_FLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) "$<" -o "$@")
 
 
 # Pull in non-recursive targets if this is a partial tree build.
 ifndef TOPLEVEL_BUILD
 include $(topsrcdir)/config/makefiles/nonrecursive.mk
 endif
 
 ################################################################################
--- a/js/xpconnect/src/Makefile.in
+++ b/js/xpconnect/src/Makefile.in
@@ -108,17 +108,17 @@ DictionaryHelpers.cpp: $(srcdir)/diction
 	  --makedepend-output $(MDDEPDIR)/dictionary_helper_gen.pp \
 	  $(srcdir)/dictionary_helper_gen.conf \
 	  event_impl_gen.conf
 
 GeneratedEvents.$(OBJ_SUFFIX): GeneratedEvents.h \
                                GeneratedEvents.cpp
 
 event_impl_gen.conf : $(srcdir)/event_impl_gen.conf.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $^ > event_impl_gen.conf
+	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) $^ -o event_impl_gen.conf)
 
 GeneratedEvents.h: $(srcdir)/dictionary_helper_gen.conf \
                    event_impl_gen.conf \
                    $(srcdir)/dictionary_helper_gen.py \
                    $(srcdir)/event_impl_gen.py \
                    $(LIBXUL_DIST)/sdk/bin/header.py \
                    $(LIBXUL_DIST)/sdk/bin/xpidl.py
 	$(PYTHON) $(topsrcdir)/config/pythonpath.py \
--- a/js/xpconnect/tests/components/js/Makefile.in
+++ b/js/xpconnect/tests/components/js/Makefile.in
@@ -17,9 +17,9 @@ MANIFEST_FILE = xpctest.manifest
 include $(topsrcdir)/config/rules.mk
 
 componentdir = js/xpconnect/tests/components/js
 
 libs:: $(JS_FILES)
 	$(INSTALL) $^ $(testxpcobjdir)/$(componentdir)
 
 libs:: $(MANIFEST_FILE)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $< > $(testxpcobjdir)/$(componentdir)/$(<F)
+	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $< -o $(testxpcobjdir)/$(componentdir)/$(<F))
--- a/layout/mathml/Makefile.in
+++ b/layout/mathml/Makefile.in
@@ -34,12 +34,12 @@ endif
 libs:: $(font_properties)
 	$(INSTALL) $^ $(DIST)/bin/res/fonts
 
 math_properties = mathfont.properties
 
 $(DIST)/bin/res/fonts/$(math_properties) $(DESTDIR)$(mozappdir)/res/fonts/$(math_properties): $(math_properties) Makefile
 	test -d $(@D) || $(NSINSTALL) -D $(@D)
 	rm -f $@
-	$(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py --marker=% $(DEFINES) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor,--marker=% $(DEFINES) $(ACDEFINES) $< -o $@)
 
 libs:: $(DIST)/bin/res/fonts/$(math_properties)
 install:: $(DESTDIR)$(mozappdir)/res/fonts/$(math_properties)
--- a/layout/media/Makefile.in
+++ b/layout/media/Makefile.in
@@ -111,17 +111,17 @@ EXTRA_DSO_LDOPTS = $(MOZALLOC_LIB) $(NSP
 OS_LIBS += $(call EXPAND_LIBNAME,usp10 ole32)
 DEFFILE = symbols.def
 endif
 
 include $(topsrcdir)/config/rules.mk
 
 ifeq (WINNT,$(OS_TARGET))
 symbols.def: symbols.def.in $(GLOBAL_DEPS)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor,$(ACDEFINES) $< -o $@)
 
 OS_LIBS += $(call EXPAND_LIBNAME, msimg32)
 
 ifdef MOZ_CUBEB
 OS_LIBS += $(call EXPAND_LIBNAME, winmm)
 endif
 
 endif
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -173,22 +173,21 @@ android-tgts = \
   package-name.txt \
   $(PP_JAVAFILES) \
   $(NULL)
 
 android-preqs = \
   Makefile.in \
   widget/GeckoView.java.frag \
   $(SERVICES_MANIFEST_FRAGMENTS) \
-  $(call mkdir_deps,$(sort $(dir $(PP_JAVAFILES)))) \
   $(NULL)
 
 $(android-tgts): % : %.in $(android-preqs)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-             $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor, \
+             $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< -o $@)
 
 res/drawable-mdpi/icon.png: $(ICON_PATH)
 	$(NSINSTALL) -D res/drawable-mdpi
 	cp $(ICON_PATH) $@
 
 res/drawable-hdpi/icon.png: $(ICON_PATH_HDPI)
 	$(NSINSTALL) -D res/drawable-hdpi
 	cp $(ICON_PATH_HDPI) $@
--- a/mobile/android/base/locales/Makefile.in
+++ b/mobile/android/base/locales/Makefile.in
@@ -57,21 +57,19 @@ strings-xml-preqs =\
   $(SYNCSTRINGSPATH) \
   $(BOOKMARKSPATH) \
   $(if $(IS_LANGUAGE_REPACK),FORCE) \
   $(NULL)
 
 $(if $(MOZ_ANDROID_SHARED_ACCOUNT_TYPE),,$(error Missing MOZ_ANDROID_SHARED_ACCOUNT_TYPE))
 
 $(dir-strings-xml)/strings.xml: $(strings-xml-preqs)
-	$(NSINSTALL) -D $(dir-strings-xml)
-	$(TOUCH) $(call mkdir_deps,$(dir-strings-xml))
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
+	$(call py_action,preprocessor, \
       $(DEFINES) \
 	  -DANDROID_PACKAGE_NAME=$(ANDROID_PACKAGE_NAME) \
 	  -DBOOKMARKSPATH="$(BOOKMARKSPATH)" \
 	  -DBRANDPATH="$(BRANDPATH)" \
 	  -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE=$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE) \
 	  -DMOZ_APP_DISPLAYNAME="@MOZ_APP_DISPLAYNAME@" \
 	  -DSTRINGSPATH="$(STRINGSPATH)" \
 	  -DSYNCSTRINGSPATH="$(SYNCSTRINGSPATH)" \
       $< \
-	  > $@
+	  -o $@)
--- a/mobile/android/installer/Makefile.in
+++ b/mobile/android/installer/Makefile.in
@@ -42,17 +42,17 @@ endif
 DEFINES += -DBINPATH=$(BINPATH)
 
 ifdef ENABLE_MARIONETTE
 DEFINES += -DENABLE_MARIONETTE=1
 endif
 
 ifdef MOZ_PKG_MANIFEST_P
 $(MOZ_PKG_MANIFEST): $(MOZ_PKG_MANIFEST_P) $(GLOBAL_DEPS)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) $< -o $@)
 ifdef MOZ_CHROME_MULTILOCALE
 	printf "\n[multilocale]\n" >> $@
 	for LOCALE in en-US $(MOZ_CHROME_MULTILOCALE) ;\
 	do \
 	  printf "$(BINPATH)/chrome/$$LOCALE$(JAREXT)\n" >> $@; \
 	  printf "$(BINPATH)/chrome/$$LOCALE.manifest\n" >> $@; \
 	done
 endif
--- a/mobile/locales/Makefile.in
+++ b/mobile/locales/Makefile.in
@@ -172,31 +172,29 @@ bookmarks-inc-array = \
     $(if $(has_mergedir),$(LOCALE_MERGEDIR)/mobile/profile/bookmarks.inc) \
     $(LOCALE_SRCDIR)/profile/bookmarks.inc \
     $(if $(has-mergedir),$(srcdir)/en-US/profile/bookmarks.inc) \
   )
 bookmarks-inc = $(firstword $(bookmarks-inc-array))
 
 bookmarks-preqs = \
   $(bookmarks-inc) \
-  $(call mkdir_deps,$(dir $(bookmarks-ts))) \
   $(src-bookmarks) \
   generic/profile/$(bookmarks).in \
-  $(topsrcdir)/config/Preprocessor.py \
   $(if $(IS_LANGUAGE_REPACK),FORCE) \
   $(GLOBAL_DEPS) \
   $(NULL)
 
 $(bookmarks-ts): $(bookmarks-preqs)
 	$(display_deps)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
+	$(call py_action,preprocessor, \
       -I $< \
       -DAB_CD=$(NO_JA_JP_MAC_AB_CD) \
       $(src-bookmarks) \
-      > $@
+      -o $@)
 
 .PHONY: bookmarks $(bookmarks)
 bookmarks: $(bookmarks)
 $(bookmarks): $(bookmarks-ts)
 	@echo "\nGenerating: $@"
 	ln -fn $< .
 
 
--- a/modules/libmar/tests/Makefile.in
+++ b/modules/libmar/tests/Makefile.in
@@ -4,17 +4,17 @@
 
 TESTROOT = $(abspath $(DEPTH))/_tests/xpcshell/$(relativesrcdir)
 
 DEFINES += -DBIN_SUFFIX=$(BIN_SUFFIX)
 
 include $(topsrcdir)/config/rules.mk
 
 libs:: unit/head_libmar.js.in
-	$(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py -Fsubstitution $(DEFINES) $(ACDEFINES) $^ > $(TESTROOT)/unit/head_libmar.js
+	$(call py_action,preprocessor,-Fsubstitution $(DEFINES) $(ACDEFINES) $^ -o $(TESTROOT)/unit/head_libmar.js)
 
 ifneq ($(OS_TARGET),Android)
 ifndef MOZ_PROFILE_GENERATE
 libs::
 	$(INSTALL) ../tool/signmar$(BIN_SUFFIX) $(TESTROOT)/unit
 	$(INSTALL) $(DEPTH)/dist/bin/$(DLL_PREFIX)nss3$(DLL_SUFFIX) $(TESTROOT)/unit
 ifndef MOZ_FOLD_LIBS
 	$(INSTALL) $(DEPTH)/dist/bin/$(DLL_PREFIX)nssutil3$(DLL_SUFFIX) $(TESTROOT)/unit
--- a/modules/libpref/src/Makefile.in
+++ b/modules/libpref/src/Makefile.in
@@ -35,12 +35,12 @@ ifeq ($(OS_ARCH), OS2)
 nsPrefService.$(OBJ_SUFFIX): nsPrefService.cpp
 	$(REPORT_BUILD)
 	@$(MAKE_DEPS_AUTO_CXX)
 	$(ELOG) $(CCC) $(OUTOPTION)$@ -c $(COMPILE_CXXFLAGS:-O2=-O1) $(_VPATH_SRCS)
 endif
 
 
 greprefs.js: $(grepref_files)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $^ > $@
+	$(call py_action,preprocessor,$(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $^ -o $@)
 
 libs:: greprefs.js
 	$(INSTALL) $^ $(DIST)/bin/
--- a/mozglue/build/Makefile.in
+++ b/mozglue/build/Makefile.in
@@ -31,17 +31,17 @@ endif
 MOZ_GLUE_LDFLAGS = # Don't link against ourselves
 
 DEFINES += -DIMPL_MFBT
 
 ifeq (WINNT,$(OS_TARGET))
 DEFFILE = mozglue.def
 
 mozglue.def: mozglue.def.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(if $(MOZ_REPLACE_MALLOC),-DMOZ_REPLACE_MALLOC) $(ACDEFINES) $< > $@
+	$(call py_action,preprocessor,$(if $(MOZ_REPLACE_MALLOC),-DMOZ_REPLACE_MALLOC) $(ACDEFINES) $< -o $@)
 
 GARBAGE += mozglue.def
 
 ifneq (,$(filter -DEFAULTLIB:mozcrt,$(MOZ_GLUE_LDFLAGS)))
 # Don't install the import library if we use mozcrt
 NO_INSTALL_IMPORT_LIBRARY = 1
 endif
 
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/action/preprocessor.py
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+
+from mozbuild.preprocessor import Preprocessor
+
+
+def main(args):
+  pp = Preprocessor()
+  pp.handleCommandLine(args, True)
+
+
+if __name__ == "__main__":
+  main(sys.argv[1:])
--- a/python/mozbuild/mozbuild/backend/configenvironment.py
+++ b/python/mozbuild/mozbuild/backend/configenvironment.py
@@ -5,17 +5,17 @@
 import ntpath
 import os
 import posixpath
 import re
 import sys
 
 from os.path import relpath
 
-from Preprocessor import Preprocessor
+from mozbuild.preprocessor import Preprocessor
 
 from ..util import (
     ensureParentDir,
     FileAvoidWrite,
     ReadOnlyDict,
 )
 
 
--- a/python/mozbuild/mozbuild/jar.py
+++ b/python/mozbuild/mozbuild/jar.py
@@ -17,17 +17,17 @@ from time import localtime
 from MozZipFile import ZipFile
 from cStringIO import StringIO
 
 from mozbuild.util import (
     lock_file,
     PushbackIter,
 )
 
-from Preprocessor import Preprocessor
+from mozbuild.preprocessor import Preprocessor
 from mozbuild.action.buildlist import addEntriesToListFile
 if sys.platform == 'win32':
     from ctypes import windll, WinError
     CreateHardLink = windll.kernel32.CreateHardLinkA
 
 __all__ = ['JarMaker']
 
 
rename from config/Preprocessor.py
rename to python/mozbuild/mozbuild/preprocessor.py
--- a/config/Preprocessor.py
+++ b/python/mozbuild/mozbuild/preprocessor.py
@@ -1,11 +1,27 @@
 """
 This is a very primitive line based preprocessor, for times when using
 a C preprocessor isn't an option.
+
+It currently supports the following grammar for expressions, whitespace is
+ignored:
+
+expression :
+  and_cond ( '||' expression ) ? ;
+and_cond:
+  test ( '&&' and_cond ) ? ;
+test:
+  unary ( ( '==' | '!=' ) unary ) ? ;
+unary :
+  '!'? value ;
+value :
+  [0-9]+ # integer
+  | 'defined(' \w+ ')'
+  | \w+  # string identifier or value;
 """
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import sys
 import os
@@ -16,33 +32,255 @@ import errno
 
 # hack around win32 mangling our line endings
 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65443
 if sys.platform == "win32":
   import msvcrt
   msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
   os.linesep = '\n'
 
-import Expression
+
+__all__ = [
+  'Context',
+  'Expression',
+  'Preprocessor',
+  'preprocess'
+]
+
+
+class Expression:
+  def __init__(self, expression_string):
+    """
+    Create a new expression with this string.
+    The expression will already be parsed into an Abstract Syntax Tree.
+    """
+    self.content = expression_string
+    self.offset = 0
+    self.__ignore_whitespace()
+    self.e = self.__get_logical_or()
+    if self.content:
+      raise Expression.ParseError, self
+
+  def __get_logical_or(self):
+    """
+    Production: and_cond ( '||' expression ) ?
+    """
+    if not len(self.content):
+      return None
+    rv = Expression.__AST("logical_op")
+    # test
+    rv.append(self.__get_logical_and())
+    self.__ignore_whitespace()
+    if self.content[:2] != '||':
+      # no logical op needed, short cut to our prime element
+      return rv[0]
+    # append operator
+    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
+    self.__strip(2)
+    self.__ignore_whitespace()
+    rv.append(self.__get_logical_or())
+    self.__ignore_whitespace()
+    return rv
+
+  def __get_logical_and(self):
+    """
+    Production: test ( '&&' and_cond ) ?
+    """
+    if not len(self.content):
+      return None
+    rv = Expression.__AST("logical_op")
+    # test
+    rv.append(self.__get_equality())
+    self.__ignore_whitespace()
+    if self.content[:2] != '&&':
+      # no logical op needed, short cut to our prime element
+      return rv[0]
+    # append operator
+    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
+    self.__strip(2)
+    self.__ignore_whitespace()
+    rv.append(self.__get_logical_and())
+    self.__ignore_whitespace()
+    return rv
+
+  def __get_equality(self):
+    """
+    Production: unary ( ( '==' | '!=' ) unary ) ?
+    """
+    if not len(self.content):
+      return None
+    rv = Expression.__AST("equality")
+    # unary
+    rv.append(self.__get_unary())
+    self.__ignore_whitespace()
+    if not re.match('[=!]=', self.content):
+      # no equality needed, short cut to our prime unary
+      return rv[0]
+    # append operator
+    rv.append(Expression.__ASTLeaf('op', self.content[:2]))
+    self.__strip(2)
+    self.__ignore_whitespace()
+    rv.append(self.__get_unary())
+    self.__ignore_whitespace()
+    return rv
+
+  def __get_unary(self):
+    """
+    Production: '!'? value
+    """
+    # eat whitespace right away, too
+    not_ws = re.match('!\s*', self.content)
+    if not not_ws:
+      return self.__get_value()
+    rv = Expression.__AST('not')
+    self.__strip(not_ws.end())
+    rv.append(self.__get_value())
+    self.__ignore_whitespace()
+    return rv
 
-__all__ = ['Preprocessor', 'preprocess']
+  def __get_value(self):
+    """
+    Production: ( [0-9]+ | 'defined(' \w+ ')' | \w+ )
+    Note that the order is important, and the expression is kind-of
+    ambiguous as \w includes 0-9. One could make it unambiguous by
+    removing 0-9 from the first char of a string literal.
+    """
+    rv = None
+    m = re.match('defined\s*\(\s*(\w+)\s*\)', self.content)
+    if m:
+      word_len = m.end()
+      rv = Expression.__ASTLeaf('defined', m.group(1))
+    else:
+      word_len = re.match('[0-9]*', self.content).end()
+      if word_len:
+        value = int(self.content[:word_len])
+        rv = Expression.__ASTLeaf('int', value)
+      else:
+        word_len = re.match('\w*', self.content).end()
+        if word_len:
+          rv = Expression.__ASTLeaf('string', self.content[:word_len])
+        else:
+          raise Expression.ParseError, self
+    self.__strip(word_len)
+    self.__ignore_whitespace()
+    return rv
+
+  def __ignore_whitespace(self):
+    ws_len = re.match('\s*', self.content).end()
+    self.__strip(ws_len)
+    return
+
+  def __strip(self, length):
+    """
+    Remove a given amount of chars from the input and update
+    the offset.
+    """
+    self.content = self.content[length:]
+    self.offset += length
+
+  def evaluate(self, context):
+    """
+    Evaluate the expression with the given context
+    """
+
+    # Helper function to evaluate __get_equality results
+    def eval_equality(tok):
+      left = opmap[tok[0].type](tok[0])
+      right = opmap[tok[2].type](tok[2])
+      rv = left == right
+      if tok[1].value == '!=':
+        rv = not rv
+      return rv
+    # Helper function to evaluate __get_logical_and and __get_logical_or results
+    def eval_logical_op(tok):
+      left = opmap[tok[0].type](tok[0])
+      right = opmap[tok[2].type](tok[2])
+      if tok[1].value == '&&':
+        return left and right
+      elif tok[1].value == '||':
+        return left or right
+      raise Expression.ParseError, self
+
+    # Mapping from token types to evaluator functions
+    # Apart from (non-)equality, all these can be simple lambda forms.
+    opmap = {
+      'logical_op': eval_logical_op,
+      'equality': eval_equality,
+      'not': lambda tok: not opmap[tok[0].type](tok[0]),
+      'string': lambda tok: context[tok.value],
+      'defined': lambda tok: tok.value in context,
+      'int': lambda tok: tok.value}
+
+    return opmap[self.e.type](self.e);
+
+  class __AST(list):
+    """
+    Internal class implementing Abstract Syntax Tree nodes
+    """
+    def __init__(self, type):
+      self.type = type
+      super(self.__class__, self).__init__(self)
+
+  class __ASTLeaf:
+    """
+    Internal class implementing Abstract Syntax Tree leafs
+    """
+    def __init__(self, type, value):
+      self.value = value
+      self.type = type
+    def __str__(self):
+      return self.value.__str__()
+    def __repr__(self):
+      return self.value.__repr__()
+
+  class ParseError(StandardError):
+    """
+    Error raised when parsing fails.
+    It has two members, offset and content, which give the offset of the
+    error and the offending content.
+    """
+    def __init__(self, expression):
+      self.offset = expression.offset
+      self.content = expression.content[:3]
+    def __str__(self):
+      return 'Unexpected content at offset {0}, "{1}"'.format(self.offset,
+                                                              self.content)
+
+class Context(dict):
+  """
+  This class holds variable values by subclassing dict, and while it
+  truthfully reports True and False on
+
+  name in context
+
+  it returns the variable name itself on
+
+  context["name"]
+
+  to reflect the ambiguity between string literals and preprocessor
+  variables.
+  """
+  def __getitem__(self, key):
+    if key in self:
+      return super(self.__class__, self).__getitem__(key)
+    return key
 
 
 class Preprocessor:
   """
   Class for preprocessing text files.
   """
   class Error(RuntimeError):
     def __init__(self, cpp, MSG, context):
       self.file = cpp.context['FILE']
       self.line = cpp.context['LINE']
       self.key = MSG
       RuntimeError.__init__(self, (self.file, self.line, self.key, context))
   def __init__(self):
-    self.context = Expression.Context()
+    self.context = Context()
     for k,v in {'FILE': '',
                 'LINE': 0,
                 'DIRECTORY': os.path.abspath('.')}.iteritems():
       self.context[k] = v
     self.actionLevel = 0
     self.disableLevel = 0
     # ifStates can be
     #  0: hadTrue
@@ -70,65 +308,65 @@ class Preprocessor:
                        'include': 0,
                        'includesubst': 0,
                        'error': 0}.iteritems():
       self.cmds[cmd] = (level, getattr(self, 'do_' + cmd))
     self.out = sys.stdout
     self.setMarker('#')
     self.LE = '\n'
     self.varsubst = re.compile('@(?P<VAR>\w+)@', re.U)
-  
+
   def warnUnused(self, file):
     if self.actionLevel == 0:
       sys.stderr.write('{0}: WARNING: no preprocessor directives found\n'.format(file))
     elif self.actionLevel == 1:
       sys.stderr.write('{0}: WARNING: no useful preprocessor directives found\n'.format(file))
     pass
 
   def setLineEndings(self, aLE):
     """
     Set the line endings to be used for output.
     """
     self.LE = {'cr': '\x0D', 'lf': '\x0A', 'crlf': '\x0D\x0A'}[aLE]
-  
+
   def setMarker(self, aMarker):
     """
     Set the marker to be used for processing directives.
     Used for handling CSS files, with pp.setMarker('%'), for example.
     The given marker may be None, in which case no markers are processed.
     """
     self.marker = aMarker
     if aMarker:
       self.instruction = re.compile('{0}(?P<cmd>[a-z]+)(?:\s(?P<args>.*))?$'
-                                    .format(aMarker), 
+                                    .format(aMarker),
                                     re.U)
       self.comment = re.compile(aMarker, re.U)
     else:
       class NoMatch(object):
         def match(self, *args):
           return False
       self.instruction = self.comment = NoMatch()
-  
+
   def clone(self):
     """
     Create a clone of the current processor, including line ending
     settings, marker, variable definitions, output stream.
     """
     rv = Preprocessor()
     rv.context.update(self.context)
     rv.setMarker(self.marker)
     rv.LE = self.LE
     rv.out = self.out
     return rv
-  
+
   def applyFilters(self, aLine):
     for f in self.filters:
       aLine = f[1](aLine)
     return aLine
-  
+
   def write(self, aLine):
     """
     Internal method for handling output.
     """
     if self.checkLineNumbers:
       self.writtenLines += 1
       ln = self.context['LINE']
       if self.writtenLines != ln:
@@ -138,17 +376,17 @@ class Preprocessor:
         self.writtenLines = ln
     filteredLine = self.applyFilters(aLine)
     if filteredLine != aLine:
       self.actionLevel = 2
     # ensure our line ending. Only need to handle \n, as we're reading
     # with universal line ending support, at least for files.
     filteredLine = re.sub('\n', self.LE, filteredLine)
     self.out.write(filteredLine)
-  
+
   def handleCommandLine(self, args, defaultToStdin = False):
     """
     Parse a commandline into this parser.
     Uses OptionParser internally, no args mean sys.argv[1:].
     """
     p = self.getCommandLineParser()
     (options, args) = p.parse_args(args=args)
     includes = options.I
@@ -163,17 +401,18 @@ class Preprocessor:
       self.out = open(options.output, 'wb')
     if defaultToStdin and len(args) == 0:
       args = [sys.stdin]
     includes.extend(args)
     if includes:
       for f in includes:
         self.do_include(f, False)
       self.warnUnused(f)
-    pass
+    if options.output:
+      self.out.close()
 
   def getCommandLineParser(self, unescapeDefines = False):
     escapedValue = re.compile('".*"$')
     numberValue = re.compile('\d+$')
     def handleE(option, opt, value, parser):
       for k,v in os.environ.iteritems():
         self.context[k] = v
     def handleD(option, opt, value, parser):
@@ -238,17 +477,17 @@ class Preprocessor:
       if cmd != 'literal':
         self.actionLevel = 2
     elif self.disableLevel == 0 and not self.comment.match(aLine):
       self.write(aLine)
     pass
 
   # Instruction handlers
   # These are named do_'instruction name' and take one argument
-  
+
   # Variables
   def do_define(self, args):
     m = re.match('(?P<name>\w+)(?:\s(?P<value>.*))?', args, re.U)
     if not m:
       raise Preprocessor.Error(self, 'SYNTAX_DEF', args)
     val = 1
     if m.group('value'):
       val = self.applyFilters(m.group('value'))
@@ -268,17 +507,17 @@ class Preprocessor:
     if len(self.ifStates) == 0 or self.ifStates[-1] == 2:
       sys.stderr.write('WARNING: bad nesting of #else\n')
   def do_if(self, args, replace=False):
     if self.disableLevel and not replace:
       self.disableLevel += 1
       return
     val = None
     try:
-      e = Expression.Expression(args)
+      e = Expression(args)
       val = e.evaluate(self.context)
     except Exception:
       # XXX do real error reporting
       raise Preprocessor.Error(self, 'SYNTAX_ERR', args)
     if type(val) == str:
       # we're looking for a number value, strings are false
       val = False
     if not val:
@@ -467,26 +706,25 @@ class Preprocessor:
     self.context['LINE'] = oldLine
     self.context['DIRECTORY'] = oldDir
   def do_includesubst(self, args):
     args = self.filter_substitution(args)
     self.do_include(args)
   def do_error(self, args):
     raise Preprocessor.Error(self, 'Error: ', str(args))
 
-def main():
-  pp = Preprocessor()
-  pp.handleCommandLine(None, True)
-  return
 
 def preprocess(includes=[sys.stdin], defines={},
                output = sys.stdout,
                line_endings='\n', marker='#'):
   pp = Preprocessor()
   pp.context.update(defines)
   pp.setLineEndings(line_endings)
   pp.setMarker(marker)
   pp.out = output
   for f in includes:
     pp.do_include(f, False)
 
+
+# Keep this module independently executable.
 if __name__ == "__main__":
-  main()
+  pp = Preprocessor()
+  pp.handleCommandLine(None, True)
rename from config/tests/unit-Expression.py
rename to python/mozbuild/mozbuild/test/test_expression.py
--- a/config/tests/unit-Expression.py
+++ b/python/mozbuild/mozbuild/test/test_expression.py
@@ -1,15 +1,15 @@
 import unittest
 
 import sys
 import os.path
 import mozunit
 
-from Expression import Expression, Context
+from mozbuild.preprocessor import Expression, Context
 
 class TestContext(unittest.TestCase):
   """
   Unit tests for the Context class
   """
 
   def setUp(self):
     self.c = Context()
rename from config/tests/unit-LineEndings.py
rename to python/mozbuild/mozbuild/test/test_line_endings.py
--- a/config/tests/unit-LineEndings.py
+++ b/python/mozbuild/mozbuild/test/test_line_endings.py
@@ -1,17 +1,17 @@
 import unittest
 
 from StringIO import StringIO
 import os
 import sys
 import os.path
 import mozunit
 
-from Preprocessor import Preprocessor
+from mozbuild.preprocessor import Preprocessor
 
 class TestLineEndings(unittest.TestCase):
   """
   Unit tests for the Context class
   """
 
   def setUp(self):
     self.pp = Preprocessor()
rename from config/tests/unit-Preprocessor.py
rename to python/mozbuild/mozbuild/test/test_preprocessor.py
--- a/config/tests/unit-Preprocessor.py
+++ b/python/mozbuild/mozbuild/test/test_preprocessor.py
@@ -1,17 +1,17 @@
 import unittest
 
 from StringIO import StringIO
 import os
 import sys
 import os.path
 from mozunit import main, MockedOpen
 
-from Preprocessor import Preprocessor
+from mozbuild.preprocessor import Preprocessor
 
 def NamedIO(name, content):
   with open(name, 'w') as f:
     f.write(content)
   return name
 
 class TestPreprocessor(unittest.TestCase):
   """
--- a/python/mozbuild/mozpack/packager/__init__.py
+++ b/python/mozbuild/mozpack/packager/__init__.py
@@ -1,13 +1,13 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-from Preprocessor import Preprocessor
+from mozbuild.preprocessor import Preprocessor
 import re
 import os
 from mozpack.errors import errors
 from mozpack.chrome.manifest import (
     Manifest,
     ManifestChrome,
     ManifestInterfaces,
     is_manifest,
--- a/python/mozbuild/mozpack/test/test_packager.py
+++ b/python/mozbuild/mozpack/test/test_packager.py
@@ -13,17 +13,17 @@ from mozpack.packager import (
     SimpleManifestSink,
 )
 from mozpack.files import GeneratedFile
 from mozpack.chrome.manifest import (
     ManifestResource,
     ManifestContent,
 )
 from mozunit import MockedOpen
-from Preprocessor import Preprocessor
+from mozbuild.preprocessor import Preprocessor
 from mozpack.errors import (
     errors,
     ErrorMessage,
 )
 import mozpack.path
 
 MANIFEST = '''
 bar/*
--- a/services/sync/Makefile.in
+++ b/services/sync/Makefile.in
@@ -5,17 +5,17 @@
 # Definitions used by constants.js.
 weave_version := 1.30.0
 weave_id      := {340c2bbc-ce74-4362-90b5-7c26312808ef}
 
 # Preprocess files.
 SYNC_PP := modules/constants.js
 SYNC_PP_FLAGS := \
  -Dweave_version=$(weave_version) \
- -Dweave_id=$(weave_id)
+ -Dweave_id="$(weave_id)"
 SYNC_PP_PATH = $(FINAL_TARGET)/modules/services-sync
 PP_TARGETS += SYNC_PP
 
 # The set of core JavaScript modules for Sync. These are copied as-is.
 sync_modules := \
   addonsreconciler.js \
   addonutils.js \
   engines.js \
--- a/toolkit/library/Makefile.in
+++ b/toolkit/library/Makefile.in
@@ -122,18 +122,18 @@ STATIC_LIBS += ipdlunittest_s
 endif
 
 ifeq (Linux,$(OS_ARCH))
 ifneq (Android,$(OS_TARGET))
 OS_LIBS += -lrt
 EXTRA_DSO_LDOPTS += -Wl,-version-script,symverscript
 
 symverscript: symverscript.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-		-DVERSION="$(LIBRARY_NAME)$(MOZILLA_SYMBOLVERSION)" $< > $@
+	$(call py_action,preprocessor, \
+		-DVERSION="$(LIBRARY_NAME)$(MOZILLA_SYMBOLVERSION)" $< -o $@)
 
 EXTRA_DEPS += symverscript
 endif
 endif
 
 STATIC_LIBS += \
   xpcom_core \
   ucvutil_s \
--- a/toolkit/locales/l10n.mk
+++ b/toolkit/locales/l10n.mk
@@ -153,18 +153,18 @@ TK_DEFINES = $(firstword \
 PKG_ZIP_DIRS = chrome $(or $(DIST_SUBDIRS),$(DIST_SUBDIR))
 
 langpack-%: LANGPACK_FILE=$(_ABS_DIST)/$(PKG_LANGPACK_PATH)$(PKG_LANGPACK_BASENAME).xpi
 langpack-%: AB_CD=$*
 langpack-%: XPI_NAME=locale-$*
 langpack-%: libs-%
 	@echo "Making langpack $(LANGPACK_FILE)"
 	$(NSINSTALL) -D $(DIST)/$(PKG_LANGPACK_PATH)
-	$(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) \
-	  -I$(TK_DEFINES) -I$(APP_DEFINES) $(srcdir)/generic/install.rdf > $(DIST)/xpi-stage/$(XPI_NAME)/install.rdf
+	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) \
+	  -I$(TK_DEFINES) -I$(APP_DEFINES) $(srcdir)/generic/install.rdf -o $(DIST)/xpi-stage/$(XPI_NAME)/install.rdf)
 	cd $(DIST)/xpi-stage/locale-$(AB_CD) && \
 	  $(ZIP) -r9D $(LANGPACK_FILE) install.rdf $(PKG_ZIP_DIRS) chrome.manifest
 
 # This variable is to allow the wget-en-US target to know which ftp server to download from
 ifndef EN_US_BINARY_URL 
 EN_US_BINARY_URL = $(error You must set EN_US_BINARY_URL)
 endif
 
--- a/toolkit/mozapps/installer/packager.mk
+++ b/toolkit/mozapps/installer/packager.mk
@@ -173,22 +173,21 @@ RPMBUILD_SOURCEDIR=$(RPMBUILD_TOPDIR)/SO
 RPMBUILD_SPECDIR=$(topsrcdir)/toolkit/mozapps/installer/linux/rpm
 RPMBUILD_BUILDDIR=$(_ABS_DIST)/..
 
 SPEC_FILE = $(RPMBUILD_SPECDIR)/mozilla.spec
 RPM_INCIDENTALS=$(topsrcdir)/toolkit/mozapps/installer/linux/rpm
 
 RPM_CMD = \
   echo Creating RPM && \
-  mkdir -p $(RPMBUILD_SOURCEDIR) && \
-  $(PYTHON) $(topsrcdir)/config/Preprocessor.py \
+  $(PYTHON) -m mozbuild.action.preprocessor \
 	-DMOZ_APP_NAME=$(MOZ_APP_NAME) \
 	-DMOZ_APP_DISPLAYNAME="$(MOZ_APP_DISPLAYNAME)" \
-	< $(RPM_INCIDENTALS)/mozilla.desktop \
-	> $(RPMBUILD_SOURCEDIR)/$(MOZ_APP_NAME).desktop && \
+	$(RPM_INCIDENTALS)/mozilla.desktop \
+	-o $(RPMBUILD_SOURCEDIR)/$(MOZ_APP_NAME).desktop && \
   rm -rf $(_ABS_DIST)/$(TARGET_CPU) && \
   $(RPMBUILD) -bb \
   $(SPEC_FILE) \
   --target $(TARGET_CPU) \
   --buildroot $(RPMBUILD_TOPDIR)/BUILDROOT \
   --define "moz_app_name $(MOZ_APP_NAME)" \
   --define "moz_app_displayname $(MOZ_APP_DISPLAYNAME)" \
   --define "moz_app_version $(MOZ_APP_VERSION)" \
--- a/webapprt/Makefile.in
+++ b/webapprt/Makefile.in
@@ -23,14 +23,14 @@ libs:: $(call mkdir_deps,$(FINAL_TARGET)
 
 GRE_BUILDID := $(shell cat $(DEPTH)/config/buildid)
 DEFINES += -DGRE_MILESTONE=$(GRE_MILESTONE) \
            -DGRE_BUILDID=$(GRE_BUILDID) \
            -DMOZ_APP_BASENAME=$(MOZ_APP_BASENAME) \
            $(NULL)
 
 webapprt.ini: application.ini.in $(DEPTH)/config/buildid $(topsrcdir)/config/milestone.txt
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $< > $@
+	$(call py_action,preprocessor,$(DEFINES) $< -o $@)
 
 libs:: webapprt.ini
 	$(INSTALL) webapprt.ini $(FINAL_TARGET)
 
 GARBAGE += webapprt.ini
--- a/webapprt/win/Makefile.in
+++ b/webapprt/win/Makefile.in
@@ -63,19 +63,19 @@ PPL_LOCALE_ARGS = \
   $(NULL)
 else
 PPL_LOCALE_ARGS=$(call EXPAND_LOCALE_SRCDIR,webapprt/locales)/webapp-uninstaller
 endif
 
 libs::
 	$(RM) -r $(CONFIG_DIR)
 	$(MKDIR) $(CONFIG_DIR)
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py -Fsubstitution \
+	$(call py_action,preprocessor,-Fsubstitution \
 		$(DEFINES) $(ACDEFINES) \
-		$(srcdir)/webapp-uninstaller.nsi.in > $(CONFIG_DIR)/webapp-uninstaller.nsi
+		$(srcdir)/webapp-uninstaller.nsi.in -o $(CONFIG_DIR)/webapp-uninstaller.nsi)
 	$(PYTHON) \
 		$(topsrcdir)/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py \
 		--create-nlf-file $(topsrcdir) $(AB_CD) $(CONFIG_DIR)
 	$(PYTHON) \
 		$(topsrcdir)/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py \
 		--preprocess-single-file $(topsrcdir) $(PPL_LOCALE_ARGS) $(CONFIG_DIR) \
 		webapp-uninstaller.properties webapp-uninstaller-locale.nsh
 	$(MAKE) webapp_uninstaller
--- a/xulrunner/app/Makefile.in
+++ b/xulrunner/app/Makefile.in
@@ -98,17 +98,17 @@ ifdef MOZ_WIDGET_GTK
 libs::
 	$(INSTALL) $(IFLAGS1) $(DIST)/branding/default16.png $(DIST)/bin/chrome/icons/default
 	$(INSTALL) $(IFLAGS1) $(DIST)/branding/default32.png $(DIST)/bin/chrome/icons/default
 	$(INSTALL) $(IFLAGS1) $(DIST)/branding/default48.png $(DIST)/bin/chrome/icons/default
 endif
 
 # XXX applications would need to supply this file
 #export:: brand.dtd.in
-#	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $^ > brand.dtd
+#	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) $^ -o brand.dtd)
 
 export::
 	$(NSINSTALL) -D $(DIST)/branding
 ifeq ($(OS_ARCH),WINNT)
 	cp $(srcdir)/xulrunner.ico   $(DIST)/branding/xulrunner.ico
 	cp $(srcdir)/xulrunner.ico   $(DIST)/branding/app.ico
 	cp $(srcdir)/document.ico  $(DIST)/branding/document.ico
 endif
--- a/xulrunner/installer/Makefile.in
+++ b/xulrunner/installer/Makefile.in
@@ -102,35 +102,34 @@ DEFINES += \
 	-DGRE_BUILDID=$(GRE_BUILDID) \
 	-DMOZ_DEB_TIMESTAMP=$(MOZ_DEB_TIMESTAMP) \
 	-DMOZ_APP_NAME=$(MOZ_APP_NAME) \
 	-Dinstalldir=$(installdir) \
 	$(NULL)
 
 ifeq ($(OS_TARGET),Linux)
 debian/changelog: $(srcdir)/debian/changelog.in  $(LIBXUL_DIST)/bin/platform.ini
-	mkdir -p debian
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ > $@
+	$(call py_action,preprocessor, \
+        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ -o $@)
 
 debian/xulrunner.links: $(srcdir)/debian/xulrunner.links.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ > $@
+	$(call py_action,preprocessor, \
+        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ -o $@)
 
 debian/xulrunner.service: $(srcdir)/debian/xulrunner.service.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ > $@
+	$(call py_action,preprocessor, \
+        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ -o $@)
 
 debian/prerm: $(srcdir)/debian/prerm.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ > $@
+	$(call py_action,preprocessor, \
+        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ -o $@)
 
 debian/postinst: $(srcdir)/debian/postinst.in
-	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
-        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ > $@
+	$(call py_action,preprocessor, \
+        $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $^ -o $@)
 
 package:
 	$(MAKE) package -C $(DEPTH)
 
 deb: package debian/changelog debian/xulrunner.service debian/xulrunner.links
 	$(NSINSTALL)  $(topsrcdir)/$(MOZ_BUILD_APP)/installer/debian .
 	rm -fr $(DEBDESTDIR)
 	$(NSINSTALL) -D $(DEBDESTDIR)/$(installdir)