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:])