/dev/trouble
Eric Roller's Development Blog

General

General development topics.

Up until today, I have been using a Build Phase script to update the CFBundleVersion in the Info.plist file using a script like this:

#!/bin/bash

if [[ ! -d .git ]]; then
  echo "Error: Git setup not found: ./.git"
  exit 1
fi

# The number of commits in the master branch.
rev=$(expr $(git rev-list master --count) - $(git rev-list HEAD..master --count))

echo "Updating build number to $rev:"

for plist in 
    "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}" 
    ; do
  if [ -f "$plist" ]; then
    echo " -> $(file "$plist")"
    /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $rev" "$plist"
    plutil -lint -s "$plist"
  else
    echo " ?? $plist"
  fi
done

Sadly, with Xcode 15, this stopped working. Whether or not ENABLE_USER_SCRIPT_SANDBOXING was enabled or not, Xcode no longer allowed access to the plist file (or complained about a circular dependency).

So, now I am using a build configuration file to set build number; it needs to be added to the Xcode project, and selected in the project "Configurations". My file contains just:

// proj.xcconfig
CURRENT_PROJECT_VERSION = 0

Also, the Info.plist file needs to be updated to use:

<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

My new methodology uses a "Build pre-actions" script (added in the target's scheme editor and using the build settings from the target) to update the .xcconfig file before every build:

#!/bin/bash
cd "$SOURCE_ROOT"

if [[ ! -d .git ]]; then
  echo "Error: Git setup not found: ./.git"
  exit 1
fi

# The number of commits in the master branch.
rev=$(expr $(git rev-list master --count) - $(git rev-list HEAD..master --count))

echo "Updating build number to $rev:"

sed -i '' "/CURRENT_PROJECT_VERSION/ { s/[[:digit:]]\{1,\}/$rev/; }" ./*.xcconfig

But since the .xcconfig file is also a revision-controlled file, I reset the value again in a "Build post-actions" script. Similar to using git restore to revert the changes:

#!/bin/bash
cd "$SOURCE_ROOT"
sed -i '' '/CURRENT_PROJECT_VERSION/ { s/[[:digit:]]{1,}/0/; }' ./*.xcconfig

Explanation of the sed code:

  • For any file matching *.xcconfig,
  • modify it "in place" (not creating a backup file),
  • all lines containing "CURRENT_PROJECT_VERSION",
  • substitute one or more digits with $rev or 0.

In my setup, fastlane usually runs on a separate computer in my closet. Therefore I find it helpful to add audio messages such that I can hear the status:

sh 'say "Creating screenshots for iOS devices."'

Fastlane's say() command sometimes hasn't worked for me. That's why I have used sh 'say'. However, that was recorded by fastlane as an action. Using this Ruby syntax is not tracked as an action:

`say "Processing images."`

But neither does its produce any text in the log. So I created this Ruby function which prints the message and then speaks it:

def talk(s) puts s; `say "#{s}"` end

With the function anywhere in fastlanes Fastfile, one can just call it like a command:

talk "Framing screenshots"

Speaking Multiple Phrases

This all works nicely, but I discovered that my error message needs to be made more prominent. I might be in the bathroom and don't hear the announcement. That's why I have now changed it to be a lot longer and a lot more annoying.

error do |lane, exception|
  phrases = [
      "Are you serious?",
      "Huston, we've had a problem!",
      "I can't believe it!",
      "I thought you had fixed that.",
      "Is it broken again?",
      "Maybe we can just reboot it?",
      "No! No! No, not again!",
      "Oops! I did it again!",
      "Something smells fishy!",
      "There is a left-over screw here.",
      "There must be a rational explanation for this.",
      "This must be a mistake!",
      "What if we pressed the red button?",
      "What is going on?",
      "What is this switch for?",
      "Where's the beef?",
      "Where is the user manual?",
      "Who pressed the red button?",
      "Who let the dogs out?",
      "You must be joking!",
  ]

  `say "#{ phrases.shuffle.join(" [[slnc 2500]] ") }"`
end

Note that the phrases are shuffled and concatenated by [[sinc 2500]] instructions which add 2.5-second-long pauses between the phrases. I found that in Apple's Speech Synthesis Programming Guide.

Whenever I like to show or hide view elements, I stumble over the hidden() view property:

struct MyView: View {
    var body: some View {
        Text("Visible")

        Text("Not visible")
            .hidden()
    }
}

What perplexes me is why anyone would need this? If it is always hidden, then why add it in the first place??

Well, I guess it could be used to create some well-defined, empty space in the view layout.

Now, this is what I would really have a use for:

struct MyView: View {
    var show_info: Bool

    var body: some View {
        Text("Visible")

        Text("Sometimes visible")
            .hide(!show_info)    // OBS! Doesn't exist!
    }
}

Depending on whether or not the hidden part should be used when laying out the view, a different approach is needed. Either without taking any space:

struct MyView: View {
    var show_info: Bool

    var body: some View {
        Text("Visible")

        if show_info {
            // Not occupying space when hidden
            Text("Sometimes visible")
        }
    }
}

Or occupying space:

struct MyView: View {
    var show_info: Bool

    var body: some View {
        Text("Visible")

        // Taking space, even if transparent
        Text("Sometimes visible")
            .opacity(show_info ? 1.0 : 0.0)
    }
}

When the NumberFormatter() is asked to output Decimal/NSDecimalNumber values with more than 15 fractional digits, the output is rounded (apparently to the precision of Double numbers). Example:

14 decimals: 3.14159265358979
15 decimals: 3.141592653589793
16 decimals: 3.1415926535897900
17 decimals: 3.14159265358979000
18 decimals: 3.141592653589790000
19 decimals: 3.1415926535897900000
20 decimals: 3.14159265358979000000

Note the trailing zeros which are not expected.

Note that Decimal numbers support a precision of 38-decimals. Example: Decimal.pi.description

3.14159265358979323846264338327950288419

Code to reproduce the rounding problem:

import Foundation

var number = Decimal.pi
let formatter = NumberFormatter()

for width in 10 ... 38 {
    formatter.maximumFractionDigits = width
    formatter.minimumFractionDigits = width
    print(String(width) + " decimals: " +
            (formatter.string(from: number as NSDecimalNumber) ?? ""))
}

Apple feedback ID: FB9835108

The calculation for 256^8 using Doubles works as expected:

(lldb) po pow(Double(256), Double(8))
1.8446744073709552e+19

However, trying to convert the result into a Decimal crashes:

(lldb) po Decimal(pow(Double(256), Double(8)))
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been returned to the state before expression evaluation.

Decimals should handle values with up to 38 digits as mantissa with exponents from –128 through 127.

Apple feedback id: FB7706665

Quite often I find myself in the Terminal and want to open Xcode with the project in the current directory:

% open CurrentProject.xcodeproj

This works just fine, of course, but with the following alias (added to ~/.cshrc), I don't need to know the name of the project, assuming there is always only one project in the current directory:

% alias xcode '\ls | grep xcodeproj | xargs open'
% xcode

N.B. The use of \ls ignores the alias that I have for 'ls'. Instead of ls *.xcodeproj, the pipe via grep avoids errors like "ls: No match.".

P.S. Knowing the name of the project is actually not the problem; since there are multiple items named "Current" or "CurrentProject" in the same folder, the issue is that I can't just type open SHIFT+C<TAB><CR> (where pressing TAB autocompletes the name); instead, I have to type: open SHIFT+C<TAB>SHIFT+P<TAB>.x<TAB><CR>.

Update

The Xcode text editor invocation tool xed can achieve much the same thing; just type:

% xed .

Similar to what I have been doing with subversion, I now also create backups for my git repositories.

Note that the git repositories on the backup host are local repositories (not cloned ones) and that they are named ".git".

From /etc/periodic/monthly/mine.monthly:

if [ -d /Library/Repositories ]; then
  cd /Library/Repositories
  for i in *.git ; do
    # Not a local git repository? Ignore it.
    if [ ! -f "$i/HEAD" ]; then
        continue
    fi
    line="/tmp/monthly-$i.out"
    # File: /Volumes/Backups/Attic/git-file-2016-08-10.bundle
    name="git-`basename $i`-`date +%F`.bundle"
    dump="/Volumes/Backups/Attic/$name"
    if [ ! -f "$dump" ]; then
      echo "Bundling git repository: $name"
      { cd "$i"; git bundle create "$dump" --all; } >& "$line"
      # Change the user for the dump file.
      chown gollum:gollum "$dump"
      # Only output the last stderr line then delete the file.
      tail -1 "$line"
      rm "$line"
    else
      echo "Warning: Dump already exists: $name"
    fi
  done
else
  echo "Error: Repositories directory not found."
fi

The weekly backups are much the same, except that only the three most recent repositories are backed up. Thus, the for loop in /etc/periodic/weekly/mine.weekly changes to:

  for i in `ls -1t *.git | sed '3 q'` ; do
    [...]
    line="/tmp/weekly-$i.out"
    [...]

N.B. I still use TrashLater to keep files for only 6 months in the Attic folder.

Blog Reboot: First Post

- Posted in General by

I have decided to switch my blog to a different backend; one that is easier to maintain and doesn't rely on MySQL (which I always found overkill and difficult to backup). Now I only need to extract my old blog posts from that last database dump file...

Update: Having worked through the dump file in a text editor, I found hundreds of duplicate entries marked as 'inherit' and 'nnn-revision-m'. These entries may be the result of multiple edits of a post. This is exactly the kind of data bloat that I strive to avoid. The current solution stores all entries in simple text files. Easy to back up. Easy to index. No duplication.

Just added a new section to /etc/periodic/monthly/mine.monthly in the attempt to create monthly backups of my subversion repositories, using compressed dump files:

[Update: Now using quoted variable names.] [Update: Now checking "format" file.]

if [ -d /Library/Repositories ]; then
  cd /Library/Repositories
  for i in * ; do
    # Not a subversion repository? Ignore it.
    if [ ! -f "$i/format" ]; then
        continue
    fi
    line="/tmp/monthly-$i.out"
    # File: /Volumes/Backups/Attic/svn-file-2014-01-17.dump.gz
    name="svn-$i-`date +%F`.dump.gz"
    dump="/Volumes/Backups/Attic/$name"
    if [ ! -f "$dump" ]; then
      echo "Dumping subversion repository: $name"
      # Dump into a gzipped file; "Dumped revision n" into temp file.
      { /usr/bin/svnadmin dump "$i" | gzip -c -9 > "$dump"; } >& "$line"
      # Change the user for the dump file.
      chown gollum:gollum "$dump"
      # Only output the last stderr line then delete the file.
      tail -1 "$line"
      rm "$line"
    else
      echo "Warning: Dump already exists: $name"
    fi
  done
else
  echo "Error: Repositories directory not found."
fi

N.B. I also use TrashLater to keep files for only 6 months in the Attic folder.

For /etc/periodic/weekly/mine.weekly I only dump the most recently modified repository since I don't normally work on multiple projects. This is done with a different foreach loop, where we get just one repository file name:

if [ -d /Library/Repositories ]; then
  cd /Library/Repositories
  # List the dir by last modified and get the first entry only.
  for i in `ls -1t | sed '1 q'` ; do
    line="/tmp/weekly-$i.out"
    # [...as above...]
  done
else
  echo "Error: Subversion repositories not found."
fi

phpMyAdmin

- Posted in General by

I see from my web-server log that people like to guess the path to phpMyAdmin in all different variations, colours, shapes and sizes. Now, this is exactly why I have not installed it in a fashion that makes it visible to anyone other than me. Better yet, come to think of it, I have not installed it at all and I don't think I ever will. But if I do, you should probably search for all variations, colours, shapes and sizes of phpMyNeatlyStashedAwayAndHiddenInstallationOfMyAdmin.