From ffb62ff07662af368e93de619898daecc53054c4 Mon Sep 17 00:00:00 2001 From: Neil Williams Date: Tue, 22 Nov 2011 12:14:48 -0800 Subject: [PATCH] Rewrite makefile to better track dependencies. --- r2/Makefile | 214 +++++++++++++++++++++++++++--------------------- r2/r2/lib/js.py | 108 ++++++++++++++++++++---- 2 files changed, 212 insertions(+), 110 deletions(-) diff --git a/r2/Makefile b/r2/Makefile index 0f003d66f..8ba1ef859 100644 --- a/r2/Makefile +++ b/r2/Makefile @@ -20,135 +20,161 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ -# Javascript files to be compressified -js_libs = $(addprefix lib/,json2.js jquery.cookie.js jquery.url.js ui.core.js ui.datepicker.js jquery.flot.js jquery.lazyload.js) -js_sources = $(js_libs) jquery.reddit.js reddit.js login.js ui.js base.js sponsored.js compact.js blogbutton.js flair.js analytics.js utils.js -js_targets = button.js jquery.flot.js sponsored.js -localized_js_targets = reddit.js mobile.js -localized_js_outputs = $(localized_js_targets:.js=.*.js) - -# CSS targets -main_css = reddit.css -main_sprite = $(static_dir)/sprite-main.png -compact_css = compact.css -compact_sprite = $(static_dir)/sprite-compact.png -css_targets = $(main_css) $(compact_css) reddit-ie6-hax.css reddit-ie7-hax.css mobile.css - -package = r2 -static_dir = $(package)/public/static - +SHELL=/bin/sh +PYTHON=python SED=sed CAT=cat -PYTHON=python -JS_COMPRESS = paster run standalone $(package)/lib/js.py -c "build_reddit_js()" -CSS_COMPRESS = $(SED) -e 's/ \+/ /' -e 's/\/\*.*\*\///g' -e 's/: /:/' | grep -v "^ *$$" -UPDATE_NAMES = $(PYTHON) $(package)/lib/static.py +BUILD_DIR=build -# If admin codebase is install, get its path so that we can build ini -# files against the primary production.ini -PRIVATEREPOS = $(shell $(PYTHON) -c 'exec "try: import r2admin; print r2admin.__path__[0]\nexcept:pass"') - -# If reddit-i18n repo is found, use that for r2.pot generation -I18NPATH = $(shell $(PYTHON) -c 'exec "try: import reddit_i18n; print reddit_i18n.__path__[0]\nexcept ImportError: print \"r2/i18n\""') - -#------ - -JSTARGETS = $(foreach js, $(js_targets) $(localized_js_targets), $(static_dir)/$(js)) -JSOUTPUTS = $(foreach js, $(js_targets) $(localized_js_outputs), $(static_dir)/$(js)) -JSSOURCES = $(foreach js, $(js_sources), $(static_dir)/js/$(js)) -CSSTARGETS = $(foreach css, $(css_targets), $(static_dir)/$(css)) -SPRITES = $(main_sprite) $(compact_sprite) -RTLCSS = $(CSSTARGETS:.css=-rtl.css) -NAMES = $(static_dir)/names.json +.PHONY: clean +all: pyx static ini +clean: clean_pyx clean_i18n clean_static +#################### Cython PYX_FILES := $(shell find . -name \*.pyx) -PYXC_FILES := $(PYX_FILES:.pyx=.c) -PYXSO_FILES := $(PYX_FILES:.pyx=.so) +PYX_C_FILES := $(PYX_FILES:.pyx=.c) +PYX_SO_FILES := $(PYX_FILES:.pyx=.so) +PYX_BUILDSTAMP := $(BUILD_DIR)/pyx-buildstamp -NAMED = $(JSOUTPUTS) $(CSSTARGETS) $(RTLCSS) $(SPRITES) -NAMELINKS = $(patsubst %.css,%.*.css,$(patsubst %.js,%.*.js,$(NAMED))) -GZIPPED = $(foreach file, $(filter %.js %.css, $(wildcard $(NAMELINKS))) $(static_dir)/js/lib/jquery.js, $(file).gz) +.PHONY: clean_pyx -INIUPDATE = $(wildcard *.update) -INIS = $(INIUPDATE:.update=.ini) +pyx: $(PYX_BUILDSTAMP) -%.ini: %.update -ifneq (,$(PRIVATEREPOS)) - ln -sf `pwd`/$< $(PRIVATEREPOS)/.. - make -C $(PRIVATEREPOS)/.. $@ - ln -sf $(PRIVATEREPOS)/../$@ . -else +$(PYX_C_FILES): %.c: %.pyx + cython $< + +# this won't rebuild pyx if you're a jerk and delete a .so file without deleting the buildstamp +# make clean && make pyx will fix this +$(PYX_BUILDSTAMP): $(PYX_C_FILES) + $(PYTHON) setup.py build_ext --inplace + touch $@ + +clean_pyx: + rm -f $(PYX_BUILDSTAMP) $(PYX_C_FILES) $(PYX_SO_FILES) + +#################### i18n +I18NPATH := $(shell $(PYTHON) -c 'from r2.lib.i18n import I18N_PATH; print I18N_PATH') +STRINGS_FILE := r2/lib/strings.py +RAND_STRINGS_FILE := r2/lib/rand_strings.py +POTFILE := $(I18NPATH)/r2.pot + +.PHONY: i18n clean_i18n + +i18n: $(RAND_STRINGS_FILE) + $(PYTHON) setup.py extract_messages -o $(POTFILE) + +$(RAND_STRINGS_FILE): $(STRINGS_FILE) + paster run standalone $(STRINGS_FILE) -c "print_rand_strings()" > $(RAND_STRINGS_FILE) + +clean_i18n: + rm -f $(RAND_STRINGS_FILE) + +#################### ini files +INIFILES := $(wildcard *.update) + +ini: $(INIFILES) + +$(INIFILES): %.ini: %.update ./updateini.py example.ini $< > $@ || rm $@ -endif +#################### Static Files +STATIC_DIR := r2/public/static -all: pyx static names $(INIS) +.PHONY: clean_static -.PHONY: pyx js css rtl static names i18n clean clean_static clean_pyx all gzip clean_gzip +static: pyx css js names -$(NAMES): $(JSTARGETS) $(CSSTARGETS) $(RTLCSS) $(SPRITES) - $(UPDATE_NAMES) $(NAMES) $(NAMED) +clean_static: clean_css clean_js clean_names -$(JSTARGETS): $(JSSOURCES) - $(JS_COMPRESS) +#### Stylesheets +CSS_COMPRESS := $(SED) -e 's/ \+/ /' -e 's/\/\*.*\*\///g' -e 's/: /:/' | grep -v "^ *$$" +CSS_SOURCE_DIR := $(STATIC_DIR)/css -$(main_sprite) $(static_dir)/$(main_css): $(static_dir)/css/$(main_css) - rm -f $@ # delete symlink so we don't just overwrite the old mangled file - $(PYTHON) r2/lib/nymph.py $< $(static_dir)/sprite-main.png | $(CSS_COMPRESS) > $@ +SPRITED_STYLESHEETS := reddit.css compact.css +PROCESSED_SPRITED_STYLESHEETS := $(addprefix $(STATIC_DIR)/, $(SPRITED_STYLESHEETS)) +SPRITES := $(addprefix $(STATIC_DIR)/, $(patsubst %.css,sprite-%.png, $(SPRITED_STYLESHEETS))) -$(compact_sprite) $(static_dir)/$(compact_css) : $(static_dir)/css/$(compact_css) - rm -f $@ # delete symlink so we don't just overwrite the old mangled file - $(PYTHON) r2/lib/nymph.py $< $(static_dir)/sprite-compact.png | $(CSS_COMPRESS) > $@ +OTHER_STYLESHEETS := reddit-ie6-hax.css reddit-ie7-hax.css mobile.css +MINIFIED_OTHER_STYLESHEETS := $(addprefix $(STATIC_DIR)/, $(OTHER_STYLESHEETS)) -$(static_dir)/%.css : $(static_dir)/css/%.css +PROCESSED_STYLESHEETS := $(PROCESSED_SPRITED_STYLESHEETS) $(MINIFIED_OTHER_STYLESHEETS) +RTL_STYLESHEETS := $(PROCESSED_STYLESHEETS:.css=-rtl.css) + +CSS_OUTPUTS = $(PROCESSED_STYLESHEETS) $(RTL_STYLESHEETS) $(SPRITES) + +.PHONY: clean_css + +css: $(CSS_OUTPUTS) + +$(MINIFIED_OTHER_STYLESHEETS): $(STATIC_DIR)/%.css: $(CSS_SOURCE_DIR)/%.css $(CAT) $< | $(CSS_COMPRESS) > $@ -$(RTLCSS): %-rtl.css : %.css +$(STATIC_DIR)/sprite-%.png $(STATIC_DIR)/%.css: $(CSS_SOURCE_DIR)/%.css + # when static file names are mangled, the original becomes a symlink to the mangled name + # remove the original file here in case it's a symlink so we don't just rewrite the old file + rm -f $(STATIC_DIR)/$*.css + $(PYTHON) r2/lib/nymph.py $< $(STATIC_DIR)/sprite-$*.png | $(CSS_COMPRESS) > $(STATIC_DIR)/$*.css + +$(RTL_STYLESHEETS): %-rtl.css : %.css $(SED) -e "s/left/>######## $@ -%.c: %.pyx - cython $< +clean_css: + rm -f $(CSS_OUTPUTS) -$(PYXSO_FILES): $(PYXC_FILES) - python setup.py build_ext --inplace +#### JS +JS_MODULES := $(shell $(PYTHON) r2/lib/js.py enumerate_modules) +JS_MODULE_BUILDSTAMPS := $(foreach module,$(JS_MODULES),$(BUILD_DIR)/$(module)-js-buildstamp) +JS_OUTPUTS := $(shell $(PYTHON) r2/lib/js.py enumerate_outputs) -pyx: $(PYXSO_FILES) +.PHONY: clean_js -static: js css rtl +js: $(JS_MODULE_BUILDSTAMPS) -js: pyx $(JSTARGETS) +$(JS_OUTPUTS): $(JS_MODULE_BUILDSTAMPS) -css: $(CSSTARGETS) $(SPRITES) +define JS_MODULE_TEMPLATE +$(BUILD_DIR)/$(1)-js-buildstamp: $$(shell $(PYTHON) r2/lib/js.py dependencies $(1)) + paster run standalone r2/lib/js.py -c "build_module('$(1)')" + touch $$@ +endef -rtl: $(RTLCSS) +# apply the module template to each of the modules +# so they source their deps from js.py and build accordingly +$(foreach module,$(JS_MODULES),$(eval $(call JS_MODULE_TEMPLATE,$(module)))) -names: $(NAMES) +clean_js: + rm -f $(JS_MODULE_BUILDSTAMPS) $(STATIC_DIR)/*.js -# Not part of 'all' target as it doesn't need to be run for the site to work, -# only when r2.pot needs updating -i18n: - paster run run.ini r2/lib/strings.py -c "print_rand_strings()" > r2/lib/rand_strings.py - python setup.py extract_messages -o $(I18NPATH)/r2.pot +#### name mangling +MANGLEABLE_FILES := $(CSS_OUTPUTS) $(JS_OUTPUTS) +MANGLE_BUILDSTAMP := $(BUILD_DIR)/mangle-buildstamp +NAMES_FILE := $(STATIC_DIR)/names.json +MANGLED_FILES := $(wildcard $(foreach file,$(MANGLEABLE_FILES),$(basename $(file)).*$(suffix $(file)))) + +.PHONY: clean_names + +names: $(MANGLE_BUILDSTAMP) + +$(MANGLE_BUILDSTAMP): $(MANGLEABLE_FILES) + $(PYTHON) r2/lib/static.py $(NAMES_FILE) $(MANGLEABLE_FILES) + touch $@ + +clean_names: + rm -f $(MANGLE_BUILDSTAMP) $(NAMES_FILES) $(MANGLED_FILES) + +#### gzip! +GZIPPABLE := $(filter %.css %.js,$(MANGLED_FILES) $(STATIC_DIR)/js/lib/jquery.js) +GZIPPED := $(addsuffix .gz,$(GZIPPABLE)) + +.PHONY: clean_gzip gzip: $(GZIPPED) -%.gz: % +$(GZIPPED): %.gz: % gzip -c $< > $@ -clean: clean_static clean_pyx clean_gzip - rm -f $(INIS) r2/lib/rand_strings.py - -clean_pyx: - rm -f $(PYXSO_FILES) $(PYXC_FILES) - -clean_static: clean_names - rm -f $(JSTARGETS) $(JSOUTPUTS) $(CSSTARGETS) $(RTLCSS) $(SPRITES) - -clean_names: - rm -f $(NAMES) $(NAMELINKS) $(static_dir)/*.md5 - clean_gzip: - rm -f $(static_dir)/*.css.gz $(static_dir)/*.js.gz $(static_dir)/js/lib/*.gz + rm -f $(GZIPPED) diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py index e56432f98..c6abb6f8a 100755 --- a/r2/r2/lib/js.py +++ b/r2/r2/lib/js.py @@ -4,7 +4,15 @@ import os.path from subprocess import Popen, PIPE import re import json -from pylons import g, c + +from r2.lib.i18n import get_available_languages + +if __name__ != "__main__": + from pylons import g, c + STATIC_ROOT = g.paths["static_files"] +else: + REDDIT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + STATIC_ROOT = os.path.join(REDDIT_ROOT, "public") script_tag = '\n' inline_script_tag = '\n' @@ -35,11 +43,19 @@ class Source(object): """An abstract collection of JavaScript code.""" def get_source(self): """Return the full JavaScript source code.""" - return NotImplementedError + raise NotImplementedError def use(self): """Return HTML to insert the JavaScript source inside a template.""" - return NotImplementedError + raise NotImplementedError + + @property + def dependencies(self): + raise NotImplementedError + + @property + def outputs(self): + raise NotImplementedError class FileSource(Source): """A JavaScript source file on disk.""" @@ -52,7 +68,7 @@ class FileSource(Source): @property def path(self): """The path to the source file on the filesystem.""" - return os.path.join(g.paths["static_files"], "static/js", self.name) + return os.path.join(STATIC_ROOT, "static", "js", self.name) def use(self): from r2.lib.template_helpers import static @@ -61,6 +77,10 @@ class FileSource(Source): path.insert(1, "js") return script_tag.format(src=static(os.path.join(*path))) + @property + def dependencies(self): + return [self.path] + class Module(Source): """A module of JS code consisting of a collection of sources.""" def __init__(self, name, *sources): @@ -78,7 +98,7 @@ class Module(Source): @property def path(self): """The destination path of the module file on the filesystem.""" - return os.path.join(g.paths["static_files"], "static", self.name) + return os.path.join(STATIC_ROOT, "static", self.name) def build(self, closure): print >> sys.stderr, "Compiling {0}...".format(self.name), @@ -93,6 +113,17 @@ class Module(Source): else: return script_tag.format(src=static(self.name)) + @property + def dependencies(self): + deps = [] + for source in self.sources: + deps.extend(source.dependencies) + return deps + + @property + def outputs(self): + return [self.path] + class StringsSource(Source): """A virtual source consisting of localized strings from r2.lib.strings.""" def __init__(self, lang=None, keys=None, prepend="r.strings = "): @@ -132,6 +163,12 @@ class LocalizedModule(Module): A StringsSource is created and included which contains localized versions of the strings referenced in the module. """ + + @staticmethod + def languagize_path(path, lang): + path_name, path_ext = os.path.splitext(path) + return path_name + "." + lang + path_ext + def build(self, closure): Module.build(self, closure) @@ -139,11 +176,16 @@ class LocalizedModule(Module): string_keys = re.findall("r\.strings\.([\w$_]+)", reddit_source) print >> sys.stderr, "Creating language-specific files:" - path_name, path_ext = os.path.splitext(self.path) - for lang in g.languages: + for lang in get_available_languages(): strings = StringsSource(lang, string_keys) source = strings.get_source() - lang_path = path_name + "." + lang + path_ext + lang_path = LocalizedModule.languagize_path(self.path, lang) + + # make sure we're not rewriting a different mangled file + # via symlink + if os.path.islink(lang_path): + os.unlink(lang_path) + with open(lang_path, "w") as out: print >> sys.stderr, " " + lang_path out.write(reddit_source+source) @@ -155,10 +197,15 @@ class LocalizedModule(Module): if g.uncompressedJS: return embed + StringsSource().use() else: - name, ext = os.path.splitext(self.name) - url = name + "." + get_lang()[0] + ext + url = LocalizedModule.languagize_path(self.name, get_lang()[0]) return script_tag.format(src=static(url)) + @property + def outputs(self): + languages = get_available_languages() + for lang in languages: + yield LocalizedModule.languagize_path(self.path, lang) + class JQuery(Module): def __init__(self, cdn_src=None): Module.__init__(self, os.path.join("js", "lib", "jquery.js")) @@ -175,6 +222,14 @@ class JQuery(Module): ext = ".js" if g.uncompressedJS else ".min.js" return script_tag.format(src=self.cdn_src+ext) + @property + def dependencies(self): + return [] + + @property + def outputs(self): + return [] + module = {} module["jquery"] = JQuery() @@ -218,10 +273,31 @@ module["flot"] = Module("jquery.flot.js", def use(*names): return "\n".join(module[name].use() for name in names) -def build_reddit_js(): - closure = ClosureCompiler("r2/lib/contrib/closure_compiler/compiler.jar") - for name in module: - module[name].build(closure) +commands = {} +def build_command(fn): + commands[fn.__name__] = fn + return fn -if __name__=="__main__": - build_reddit_js() +@build_command +def enumerate_modules(): + for m in module: + print m + +@build_command +def dependencies(name): + for dep in module[name].dependencies: + print dep + +@build_command +def enumerate_outputs(): + for m in module.itervalues(): + for output in m.outputs: + print output + +@build_command +def build_module(name): + closure = ClosureCompiler("r2/lib/contrib/closure_compiler/compiler.jar") + module[name].build(closure) + +if __name__ == "__main__": + commands[sys.argv[1]](*sys.argv[2:])