KISS

Keep It Simple Stupid

Makefile to minify JS & CSS

| comments

Update on 2013-09-03: thanks to Xaero’s comment, I’ve added an example of Makefile minifying JavaScript files into a single one. Check at the end of the post.

If an iOS project contains some HTML/JS/CSS files in the package, it makes sense to minify/obfuscate them in the final .ipa file, which somewhat hinders the reverse engineering and probably decreases loading time. What follows in my solution to that point.

Suppose we have this src directory with the web application, and src/index.html is the main file. We could decide to minify them in the build products directory (somewhere /Users/user/Library/Developer/Xcode/DerivedData/appname-someletters/Build/Products/Debug-iphoneos/appname.app/), and that would isolate the converted files from the source. However, if you have over a few dozens of JS files, it quickly becomes expensive to run that process all over again with each build. Thus, we will go the other way.

Makefile

Since the make tool comes with OS X, a Makefile is a natural choice here, which won’t require any extra software. It will be integrated into the project’s build process. The Makefile creates a mirror hierarchy in src.min of that inside the src directory. It’s the first time I’ve written more or less complex Makefile, and so some decisions may not be optimal, but it works!

(Makefile_minify) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# This Makefile:
# 1. [target: js] minimizes all .js files in src/ (except in lib/ and
# legacy-related files) and stores them in a parallel hierarchy in src.min/
# 2. [target: css] minimizes .css files in src/, as specified in index.html
# (except legacy.css and commented lines), and compresses into one in src.min/css/
#
# By default, the both targets are made.
#
# http://www.egeek.me/

# the directory with the source JS & CSS files
root_dir = src
# the result dir suffix that is appended the root dir name
min_suffix = .min


# here we get all the .js files (except the exceptions) as a list
JS_TARGETS = $(shell find $(root_dir) -type f -name '*.js' ! \( -path '*/lib/*' -or -path '*legacy*' \))
# the same list, but the base directory is with the suffix
JS_MIN_TARGETS = $(JS_TARGETS:$(root_dir)/%=$(root_dir)$(min_suffix)/%)

# here we get the .css files (except the exceptions) from the index.html as a list
CSS_TARGETS = $(shell sed -nE '/\.css/ { /(legacy\.css|<!--.*-->)/ { d; }; s/.*href="([^"]+)".*/$(root_dir)\/\1/; p; }' $(root_dir)/index.html)
# the resulting one CSS file
CSS_MIN_TARGET = $(root_dir)$(min_suffix)/css/all.min.css

# set compressors and their options
JS_COMP = java -jar ~/bin/js_compiler.jar
JS_COMP_FLAGS = --charset UTF-8 --jscomp_off internetExplorerChecks

CSS_COMP = java -jar ~/bin/yuicompressor.jar
CSS_COMP_FLAGS = --type css


# targets
default: js css

.PHONY: js css

js: $(JS_MIN_TARGETS)

# creates the directories for the output file
dir_guard = @mkdir -p $(@D)

# "It's a kind of magic"
$(root_dir)$(min_suffix)/%.js: $(root_dir)/%.js
  $(dir_guard)
  $(JS_COMP) $(JS_COMP_FLAGS) --js_output_file $@ $<

css: $(CSS_MIN_TARGET)

$(CSS_MIN_TARGET): $(CSS_TARGETS)
  $(dir_guard)
  @cat $^ | $(CSS_COMP) $(CSS_COMP_FLAGS) -v -o $@

clean:
  $(RM) -rf $(root_dir)$(min_suffix)

It depends on the Java Runtime Environment, and two compressors: Closure Compiler for JavaScript, and YUI Compressor for CSS. Place them as ~/bin/jscompiler.jar and ~/bin/yuicompressor.jar respectively, or change the filenames in the file. The provided Makefile makes some specific exceptions for the project, and I left them in place in case you need something similar.

You should rename the file to Makefile after downloading. The file is commented, and if you have any further questions, leave a comment.

Xcode build

To run the make command on each build I’ve created a simple shell script and added it as a build step. Here’s the relevant part:

(compress.sh) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/sh
#
# http://www.egeek.me/

function die () {
  echo $@
  exit 1
}

cd "<where your Makefile is>"

DSTDIR="${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}"
[[ "$DSTDIR" == "" ]] && die "Can't determine the destination directory for web files. Please check that the script is run within Xcode build process."

echo "Checking the tools…"
[ -r ~/bin/js_compiler.jar ] || die "Warning! Closure Compiler is not available. Please install it here:" ~/bin/js_compiler.jar
[ -r ~/bin/yuicompressor.jar ] || die "Warning! YUI Compressor is not available. Please install it here:" ~/bin/yuicompressor.jar

# this should minify & compress all js & css files into src.min/

# get number of cores
cores=$(/usr/sbin/system_profiler -detailLevel full SPHardwareDataType | awk '/Total Number of Cores/ {print $5};')
# … or 5
cores=${cores:-5}
echo "Compressing all js and css files…"
# replace WARNING with note so that Xcode won't consider the warnings errors
make -j${cores} 2>&1 | sed 's/WARNING/note:/'

echo "Copying minified web resources to $DSTDIR…"
cp -a src.min/{main,css,localization} "$DSTDIR"

NB! The original files (in src) are in the Xcode project, but not added to any target, because the script copies the minified versions itself.

Minifying js files into a single one

To minify the project’s js files into one, we pass all of them to Closure Compiler as arguments. Here’s a sample Makefile:

(Makefile_minify_singlejs) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# http://www.egeek.me/

root_dir = src
min_suffix = .min

JS_TARGETS = $(shell find $(root_dir) -type f -name '*.js' ! \( -path '*/lib/*' -or -path '*legacy*' \))
JS_MIN_TARGET = $(root_dir)$(min_suffix)/all.js

BIN_PATH = ~/bin

JS_COMP = java -jar $(BIN_PATH)/js_compiler.jar
JS_COMP_FLAGS = --charset UTF-8 --jscomp_off internetExplorerChecks


default: js

.PHONY: js

js: $(JS_MIN_TARGET)

dir_guard = @mkdir -p $(@D)

$(JS_MIN_TARGET): $(JS_TARGETS)
  $(dir_guard)
  @$(JS_COMP) $(JS_COMP_FLAGS) --js_output_file $@ $^

clean:
  $(RM) -rf $(root_dir)$(min_suffix)

Here, in line 4, we find all *.js files in source directory, and use in the found, nondeterministic order. Most likely, there is an index.html file that includes the javascripts in some correct order. In that case, change line 4 to:

1
JS_TARGETS = $(addprefix $(root_dir)/, $(shell grep '\.js' $(root_dir)/index.html | grep -o 'src=".*"' | egrep -v '(lib|legacy)/' | cut -d'"' -f2))

When using this variant, check that the JS_TARGETS list is grep‘ed correctly.

Please leave a comment if something is unclear here, or you know a better way, or you just want to say something! :)

dev, iOS

Don't hesitate to leave a comment below. NB! If you don't see a comment form under the post, it's most likely that an extension (such as Ghostery, NoScript, or AdBlock) of your browser blocks the scripts from disqus.com, and you can unblock that.

« Time Machine-like backups in Linux ClipMenu and passwords »

Comments