Using Make to generate Chrome extension icons

File this under “you don’t have to use JS for everything”.

Problem

I’ve built a few Chrome browser extensions, and there’s a part of making them that I generally find rather tedious: icons. To account for all the different places where icons are used, as well as multiple screen resolutions, 6 differently-sized icon files are required. Sometimes I’ve manually exported a few of these from Photoshop for one-off projects, but my most recent extension required a few updates to the icon. This was the tipping point for adding automation.

In this post I’ll detail how I used only a few lines of a Makefile to handle all icon size conversions. Yep, Make. No JavaScript-based build system with a multitude of npm modules and seconds of overhead for every run. Sometimes the old ways are the best.

Solution

Taking advantage of some special features of Make, this is what I came up with:

iconsrc := src/icon-512.png
icondir := chrome-extension/icons
iconsizes := {16,19,38,48,128,256}
iconfiles := $(shell echo $(icondir)/icon-$(iconsizes).png)

$(icondir)/icon-%.png:
    @mkdir -p $(@D)
    convert $(iconsrc) -resize $* [email protected]

icons: $(iconfiles)

.PHONY: icons

That’s the whole thing. Now running make icons will generate 6 icons in under a second (on my machine at least), using ImageMagick’s convert command for the actual file generation:

$ time make icons
convert src/icon-512.png -resize 16 chrome-extension/icons/icon-16.png
convert src/icon-512.png -resize 19 chrome-extension/icons/icon-19.png
convert src/icon-512.png -resize 38 chrome-extension/icons/icon-38.png
convert src/icon-512.png -resize 48 chrome-extension/icons/icon-48.png
convert src/icon-512.png -resize 128 chrome-extension/icons/icon-128.png
convert src/icon-512.png -resize 256 chrome-extension/icons/icon-256.png

real    0m0.608s
user    0m0.157s
sys     0m0.065s

There are a few bits of potentially unfamiliar syntax in there, so I’ll break it down into chunks.

iconsizes := {16,19,38,48,128,256}
iconfiles := $(shell echo $(icondir)/icon-$(iconsizes).png)

These lines set up variables that refer to the desired icon sizes and file names. The iconsizes variable uses shell expansion to build a list of pixel widths. Anything that refers to that variable will enumerate the numbers to produce some output. This can be seen with the iconfiles variable, which generates a list of icon files (chrome-extension/icons/icon-16.png chrome-extension/icons/icon-19.png ...).

$(icondir)/icon-%.png:

This builds any of the desired icon files. The % symbol is a wildcard that matches any series of characters. The key thing is that this match can then be referenced in the subsequent instructions.

@mkdir -p $(@D)

Ensure that the icons directory exists before doing anything with it. Prefixing a command with @ is a way of silencing the command, so that it doesn’t appear in the output log. Make has several special variables that are available within each ruleset, based on the defined file(s). One of these is $(@D), which equates to “the directory part of the target filepath”. The target is the file being built, so in total this line just means “create the directory where the file to be built will be put, but only if it doesn’t already exist”.

convert $(iconsrc) -resize $* [email protected]

The part where the actual resizing happens. I’ll break this down further:

  • convert $(iconsrc) — The convert command is provided by ImageMagick, and can perform a whole bunch of transformation operations on a source image. $(iconsrc) is just referring to a variable set up at the top of the Makefile, which is the single source image I made at a large size (512px wide).
  • -resize $* — Tells convert to resize the source image to a certain pixel width… but what width? This is where the $* syntax helps. It’s another special variable built in to Make, which refers to the part of the file matched by the % symbol in the target declaration. For example, if the file name is icon-38.png, the $* variable contains just 38.
  • [email protected] — Yet another Make special variable, this time referencing the whole target filepath. In other words, this matches the exact file being built.

Putting it all together, running make chrome-extension/icons/icon-38.png will end up calling:

convert src/icon-512.png -resize 38 chrome-extension/icons/icon-38.png

So far the file generation commands have assumed a single file being built, but I have 6 different icon sizes, and I want to build them all with a single command. That’s where this magic line comes in:

icons: $(iconfiles)

It may not look like much, but this is the part that makes the automation worthwhile. This is an empty ruleset (i.e. it has no commands to run on its own) but has a dependency on $(iconfiles). As described above, $(iconfiles) is actually a list of all the desired icon filenames. Therefore running make icons will ensure that each of the icon files is built in turn by the $(icondir)/icon-%.png ruleset above it.

.PHONY: icons

One final touch — because Make is designed to work with actual files, and there is no file named icons, it’s best to add icons to a special .PHONY command. This tells Make that it shouldn’t look for any file named icons.

In summary

The beauty of Make (that is not often replicated by build tools in other languages) is that it will only build the files that need to be built. If the source file hasn’t changed since the last run, and the generated files are all found, Make will exit early saying it has nothing to do. If one of the icon files gets deleted, running make icons will re-generate only that file. But if the source image changes, make icons will generate 6 new icons for me.

This may not be the “best” solution, but it’s working well for me so far.

Credits: A lot of my knowledge of how to do this came from reading Isaac Schlueter’s Makefile tutorial.