About | Buy | Forum | Industry Watch | Learning Curve | Products | Search | Twitter | Xnews
Home » Learning Curve » Developers Workshop

Cocoa Bloat: How You Get Rid of It

Dissecting the Trimmit script. It helps understanding what you're doing.


Buy It

Try It

As Trimmit 0.95 is readied for release it's high time developers took a look at what's inside. Especially for developers the idea of constructing a script of their own [which they could for example run through CLIX] is an even more facile prospect.

Trimmit itself is based on the original 'trim-app' script but has been beefed up with diagnostic feedback for the GUI wrapper. Its functionality remains the same.

Not all the options of the Trimmit script are to be used by developers: users can make assumptions developers cannot. Localisation files and alternative binaries cannot be scraped out until the product reaches its destination.

But there are still things a conscionable developer must do. In fact there's quite a lot.

1. zsh

The Trimmit script uses zsh because the author (Ankur Kothari) quite simply believes it's the most powerful Unix shell going today [and far better than bash]. YMMV. Only full paths are used; the script requires no privilege escalation whatsoever.

#!/bin/zsh
emulate -L zsh; setopt extendedglob

2. Architectures

The next part of the script is used only on the initial GUI 'drop': its results tell the GUI wrapper which CPU architectures are present; the wrapper then populates the combo box with these results.

The script is able to distinguish between this call and subsequent calls by checking the first argument passed.

if [[ -f = $1 ]]; then
    autoload zargs;
    zargs $2/**/*(*) -- /usr/bin/file | /usr/bin/grep \(for\ architecture
    | /usr/bin/sed -E "s/.*architecture ([a-z0-9_A-Z]+)\).*/\1/g" | /usr/bin/sort | /usr/bin/uniq
    exit
fi

3. Checking Options

User options set by the combo box and the tick boxes in the wrapper are passed to the script. The script now gets more than one argument and thereby knows it's a real run and so starts parsing the arguments.

# Check the options - what do we have to do?
while getopts :dstrbl:a: o; do
    case $o in
    d) junk=1;;
    t) tif=1;;
    r) res='';;
    b) bkp=1;;
    s) sym=1;;
    l) lang=$OPTARG;;
    a) arch=$OPTARG;;
    \?) echo error; exit;;
    esac
done

The list of possible arguments are given on the 'while getopts' line. As each argument is parsed it flows through a switch whereby 'd' is interpreted to mean 'trim junk files', 't' is interpreted to mean 'trim TIFF images', and so forth. The arguments 'l' and 'a' are a bit special: they assume a following argument as well. The 'l' argument is followed by the default languages in use on the computer (by the current user - these are determined programmatically and passed to the script) and the 'a' argument assumes a CPU architecture description suitable for lipo will follow.

4. Anything to Trim?

The script next performs a sanity check: all is for naught if there's no bundle to trim! The syntax seems arcane but in fact is not: it follows ordinary C conditional expression operator binding - which is why the operands bound by '&&' are enclosed in parentheses: if and only if the first part of the expression is not true will the second part be evaluated - resulting in an exit.

# remove options from the arg list
((OPTIND > 1)) && shift $((OPTIND - 1)) || {echo FAILED - no app to trim.; exit}

5. Check Permissions

Things have to be writable if they're to be trimmed. If they are the script will notify that operations are commencing. Note this sanity check also eliminates certain categories of 'non-native' applications that can't be trimmed anyway.

# $* means all the options
for dir in $*; do
# the app itself should be a directory (and writable?)
if ! { [[ -d $dir ]] && [[ -w $dir ]] }; then
    echo "FAILED - check permissions."; exit;
fi;

echo "Trimming '$dir'.\n"

6. The 'Working Copy'

The Trimmit script operates on a separate 'temporary' or 'working' copy of the target bundle. This copy is placed in the temporary directory /tmp [/private/tmp]. As the copy is created junk files are filtered out if this is a user option. rsync is used to make the copy. If the user opted to trim junk files and extended attributes this is reported back to the wrapper.

# path to the 'working copy' of the app in /tmp
tmpapp=/tmp/${dir:t}
# create the working copy, excluding junk files if specified
/usr/bin/rsync -Ca${res-E} --exclude-from=- --delete-excluded "$dir/" $tmpapp <<<${junk + 'classes.nib
info.nib
data.dependency
.DS_Store
Headers/
PrivateHeaders/
*.h
pbdevelopment.plist'}

echo -n ${junk+"== Removing useless junk files.\n"}${res+"== Clearing extended attributes.\n"}

7. Getting Down To It

The script now switches to the directory of the temporary copy; failure here means permissions are still not adequate and the script will exit. [They should be OK at this point but entering the directory of the target is a prerequisite for performing any of the following operations; all system error codes must be checked at all times.]

# into our 'working copy'
cd $tmpapp || {echo "FAILED - check permissions."; /bin/rm -rf $tmpapp; exit}

8. Broken Symlinks

Some 'sloppy' software packages contain symbolic links to junk. Naturally even these symlinks are junk and are therefore removed.

# this removes broken symlinks (if 'remove junk' is set)
[[ -n $junk ]] && /bin/rm -fv **/*(-@N) 2>/dev/null

9. Unused Localisation Files

Localisation files are fine on the download but consume inordinate disk free space on arrival. If you're not going to be flipping your computer into twenty different languages ten times a day these files are only cramping your style. The snippet enumerates the 'language project' subdirectories in Resources and then eliminates the ones that don't conform to the system configuration.

# remove foreign languages
[[ -n $lang ]] && {
    echo "== Clearing foreign languages."
    for line in **/Resources(/N); do
        integer count = "`print -l $line/*.lproj(/N) | wc -l`"
        ((count > 1)) && {
            integer left = "`eval print -l "${(q)line}/*.lproj~${(q)line}/($lang)*(/N)" | wc -l`"
            integer right = "`eval print -l "${(q)line}/*.lproj(N)" | wc -l`"
            ((left < right)) && eval /bin/rm -vrf "${(q)line}/(^($lang)*).lproj(/N)" | grep -v lproj/
        }
    done
}

10. Liposuction!

The Apple tool for removing binaries for unused (absent) CPU architectures is lipo - aptly named as these 'universal' binary globs were once called 'fat binaries' by NeXT.

This is also where the original Trimmit script hit a snag: Apple forgot to change the nomenclature from 'fat' to 'universal' on both PPC and Intel platforms. Today the Trimmit script takes care of both the 'new' Intel version and the 'deprecated' version still found on older PPC boxes.

# lipo universal binaries
[[ -n $arch ]] && {
    echo "== Trimming universal binaries."
    # get list of fat files
    lines = `/usr/bin/file **/*(*N) | /usr/bin/grep -e 'fat file' -e 'universal binary'
            | /usr/bin/sed -E 's/: *(set.id )?Mach.*//g'`

    # for each fat file...
    for line in ${(s.
    .)lines}; do
        # make sure we're "+w" (this may be unnecessary but safety first)
        [[ -w $line:h ]] || /bin/chmod ug+w $line:h;[[ -w $line ]] || /bin/chmod ug+w $line
        # output to 'log'
        echo "   -- '$line'"
        # do lipo to /tmp. '2>&1' means that error messages are printed the standard out (our logview)
        if /usr/bin/lipo $line -thin $arch -output "/tmp/${line:t}.lipo" 2>&1; then
            # Copy the lipo to the app
            /bin/cp "/tmp/${line:t}.lipo" $line 2>&1
            # Get rid of the temp lipo file
            /bin/chmod +w "/tmp/${line:t}.lipo" && /bin/rm -f "/tmp/${line:t}.lipo"
        fi
    done
}

11. Debug Symbols

Why developers ship application binaries with debug symbols is a mystery. Debug symbols are used only during the 'development' phase of software production - when the product is not yet ready to go out the door. Why more users don't understand what junk they're harbouring on their computers is also a mystery. It's a simple configuration option in systems development. Apple further recommend giving 'install builds' to clients which are further trimmed and configured for ultimate safety.

[[ -n $sym ]] && {
    # strip debug symbols. '2>&1' means error messages to the standard out.
    /usr/bin/strip -Sur Contents/MacOS/*(*) 2>&1
    /usr/bin/strip -Sx **/*.dylib(N) **/*.framework/**/*(*N) 2>/dev/null
    echo "== Stripping debug symbols."
}

12. Compressing TIFFs

One of the few areas where the otherwise meticulous Apple engineers themselves lapse. But let's be reasonable here: Apple take care of thousands of image files, authors of individual applications but a few. TIFF ('tagged image file format') images can often be advantageously compressed without any loss of detail. The compression is performed with tiffutil, a tool present on all OS X machines. zsh stat is used for superior speed.

[[ -n $tif ]] && {
    echo "== Compressing TIFF images."
    zmodload zsh/stat # zsh stat is much, much faster
    # the (.NLk-500Uw,GI,W):
    # "Lk-500" means under 500kb, "Uw,GI,W" checks if we can write.
    # The latter may not be necessary now as we use "cp".

    for line in **/*.(#i)tif(f|)(.NLk-500Uw,GI,W); do
        if /usr/bin/tiffutil -lzw $line -out "/tmp/${line:t}" 2>/dev/null; then
            (( `stat -L +size /tmp/${line:t}` < `stat -L +size $line` ))
                    && /bin/cp "/tmp/${line:t}" $line && echo $line
            /bin/chmod +w "/tmp/${line:t}" && /bin/rm -f "/tmp/${line:t}"
        fi
    done
}

13. Bringing It Back

Now it's time to copy things back to the 'old neighbourhood'. Perhaps the user didn't want a backup even though it's default. [And in such case he's been duly warned by the wrapper, thank you.]

# now to copy our changes back to the real app
cd $dir/..
# if we're making a backup, get a name that doesn't exist
# by adding 'backup' to the end of the name of the app.
appcopy=${dir:t:r}
[[ -n $bkp ]] && {
    while [[ -d $appcopy.${dir:e} ]];do appcopy=$appcopy\ backup;done
    # the original app becomes the backup
    /bin/mv $dir:t "$appcopy.${dir:e}"
    # out trimmed app takes its place
    /bin/mv $tmpapp $dir:t
} || {
    # we're not making a backup
    # remove the real app and replace with trimmed app
    # otherwise if we can't, make our app "appname trimmed.app"
    /bin/rm -rf ${dir:t} && /bin/mv $tmpapp ${dir:t} || {while [[ -d $appcopy.${dir:e} ]]; do
        appcopy=$appcopy\ trimmed;
    done;
    /bin/mv $tmpapp "$appcopy.${dir:e}";
    echo "==\nALERT: Could not delete $dir:t - please delete this.
            Trimmed app saved to '$appcopy.${dir:e}'"}
}

14. We're Done

All that remains at this point is to print the afterword and return control to the GUI wrapper.

# and we're done
echo "\n== Trim completed =="
done

15. What Developers Must Do

It's possible for software houses to use Trimmit on their 'release' products but they should already have their own more customised routines in place. Debug symbols, poorly crafted binaries, bloated TIFFs, superlame header files from attached frameworks - all these things must be removed before 'shipping'. The recommendation of this site is to return software not courteously packaged in this fashion.

16. What Clients Should Watch Out For

Clients (users) should be treated with a bit more deference and respect. They can achieve this by demanding it.

See Also
Vacuous Virtuoso: trim-app
The Good: Trimmit — Give a Damn
Industry Watch: Trimmit vs Xslimmer
Vacuous Virtuoso: Download Trimmit
Vacuous Virtuoso: Trimmit Homepage
Industry Watch: Trimmit — Cheap 'n' Easy
Learning Curve: Why Cocoa Bloat is Nonsense
Vacuous Virtuoso: Mac developer? Clean up your app
Developers Workshop: Building and Packaging Native OS X Applications

About | Buy | Forum | Industry Watch | Learning Curve | Products | Search | Twitter | Xnews
Copyright © Rixstep. All rights reserved.