Showing posts with label AppTapp Installer. Show all posts
Showing posts with label AppTapp Installer. Show all posts

Saturday, June 07, 2008

Installer.app and chmod/chown

How is it that I would go about chmodding the directories created by CopyPath?

It seems that if the .zip contains the permissions already, they are maintained when used by CopyPath.

In other words, if you have:

foo/bar

in your .zip, and do a CopyPath("foo", "~/foo") in the XML document, the permissions are set up correctly. At least I haven't had any problems if my installer looks like that.

However, if you create implicitly create directories by using CopyPath on the individual files, they are created without permissions, and a chmod/chown is needed. For example, CopyPath("foo/bar", "nuts/baz/bar") will create "nuts/baz", with no permissions.

I have to do this with the Gutenberg SciFi and Baen Books libraries, since I don't host the .zips themselves.

To manage the chmod/chown problem, the packages ensure that the BSD subsystem is installed (or Cydia), and then runs them as part of the install steps.

Here is an example:

      <key>preflight</key>
      <array>
        <array>
          <string>IfNot</string>
          <array>
              <array>
                <string>InstalledPackage</string>
                <string>com.natetrue.iphone.iphone_binkit</string>
              </array>
          </array>
          <array>
              <array>
                <string>AbortOperation</string>
                <string>You must install the "BSD Subsystem" package from Installer first!  It's in the System Category.</string>
              </array>
          </array>
        </array>
      </array>

Which checks for the BSD subsystem as part of the pre-install. If the BSD subsystem is not installed, the user gets a pop-up error message and the package will not install.

And then the install

      <key>install</key>
      <array>
        <array>
          <string>CopyPath</string>
          <string>1013.txt</string>
          <string>~/Media/EBooks/The_First_Men_in_the_Moon/1013.txt</string>
        </array>
        <array>
          <string>If</string>
          <array>
              <array>
                <string>FirmwareVersionIs</string>
                <array>
                  <string>1.1.3</string>
                  <string>1.1.4</string>
                </array>
              </array>
          </array>
          <array>
              <array>
                <string>SetStatus</string>
                <string>Changing Permissions</string>
              </array>                              
              <array>
                <string>Exec</string>
                <string>/bin/chmod -R 755 /var/mobile/Media/EBooks</string>
              </array>
              <array>
                <string>Exec</string>
                <string>/usr/bin/chown -R mobile /var/mobile/Media/EBooks/.</string>
              </array>
              <array>                 
                <string>SetStatus</string>
                <string>Running GutenMark</string>
              </array>
              <array>
                <string>Exec</string>
                <string>/usr/bin/GutenMark --profile=en --no-toc --config=/etc/GutenMark.cfg /var/mobile/Media/EBooks/The_First_Men_in_the_Moon/1013.txt /var/mobile/Media/EBooks/The_First_Men_in_the_Moon/1013-h.htm</string>
              </array>
              <array>
                <string>SetStatus</string>
                <string>Running GutenSplit</string>
              </array>
              <array>
                <string>Exec</string>
                <string>/usr/bin/GutenSplit --no-toc /var/mobile/Media/EBooks/The_First_Men_in_the_Moon/1013-h.htm /var/mobile/Media/EBooks/The_First_Men_in_the_Moon/Chapter_</string>
              </array>
          </array>
        </array>

That's not the full install segment, but it should give you an idea. I have to separate 1.1.3/4 from earlier firmwares because they use different users and store files differently.

To help, I've created a little perl library that allows me to write packages without having to mess with the nasty XML. It's part of the iphoneebookrepo project over at google code. Specifically PlistMaker.pm.

In terms of the perl library, it ends up looking like this:

my $gutenberg_html_install =
 p_if(IsFirmwareVersion("1.1.3", "1.1.4"),
      SetStatus("Changing Permissions").
      Exec("/bin/chmod -R 755 /var/mobile/Media/EBooks").
      Exec("/usr/bin/chown -R mobile /var/mobile/Media/EBooks/.").
      SetStatus("Running GutenSplit").
      Exec("/usr/bin/GutenSplit -1 -2 -3 -4 --no-toc --no-skip /var/mobile/Media/EBooks/$BookDir/${BookNumber}-h.htm /var/mobile/Media/EBooks/$BookDir/Chapter_")
 ).
 p_if(IsFirmwareVersion("1.0.0", "1.0.1", "1.0.2", "1.1.1", "1.1.2" ),
      SetStatus("Changing Permissions").
      Exec("/bin/chmod -R 755 /var/root/Media/EBooks") .
      SetStatus("Running GutenSplit").
      Exec("/usr/bin/GutenSplit -1 -2 -3 -4 --no-toc --no-skip /var/root/Media/EBooks/$BookDir/${BookNumber}-h.htm /var/root/Media/EBooks/$BookDir/Chapter_")
 );

my %BookDescriptor = (
 bundleIdentifier => "ca.pollock.gutenberg.$BookNumber",
 name => $BookTitle,
 version => "1.0",
 location => $BookURL,
 category => "Gutenberg SciFi (" .$BookLang . ")",
 size => $BookSize,
 hash => $BookMD5,
 url => "http://jason-pollock.blogspot.com/",
 maintainer => "Jason Pollock",
 contact => 'jason@pollock.ca',
 description => "by $BookAuthor",
 install => $Book_install,
 preflight => $Book_preflight,
 postflight => "" ,
 uninstall => $Book_uninstall,
 update => ""
 );

my $BookPLIST = CreatePackage(%BookDescriptor);

One "Gotcha" to be aware of... The Exec command is actually performing it's own parameter splitting prior to calling "Exec". It does not do any sort of "~" expansion, nor does it understand spaces. So, if you are doing a chmod on a directory with spaces in the name, it won't work, regardless of escapes or quotes.

Friday, March 28, 2008

Announcing the iPhone eBook Repository

Installing eBooks onto the iPhone for reading was nearly impossible. You needed to download third party tools, use scp, rename files, the works. To help, I decided to package existing eBooks for viewing on the iPhone.

Since this is an AppTapp repository, the books can be installed onto the iPhone from any location that the device has a valid network connection, GPRS, EDGE or WiFi.

The current repository provides installation instructions for 117 SciFi novels from the Baen Free Library, with plans to add additional sources. I am currently working on Project Gutenberg.

To make use of the repository:

  1. Obtain an iPhone.
  2. Jailbreak the iPhone, I recommend ziphone for this task.
  3. Install Books, the iPhone ebook reader using the Installer application (http://code.google.com/p/iphoneebooks/ (1.37 only!))
  4. Add the repository to Installer:
    1. Start Installer
    2. Click "Sources"
    3. Click "Edit"
    4. Click "Add"
    5. Enter "http://library.pollock.ca/baen_books"
    6. Click "OK"
    7. Click "Done"
    8. Click "Refresh"
  5. Select "Install" at the bottom of the screen. You will see "Baen Free Library" is now available as an application category!

Just to clarify a couple of things. I am not providing copies of the .zip archives, they are provided to the iPhone by Webscription and Baen. The content of the files themselves are unchanged, although I have had to rename the html files to better fit in with the reader (I changed the SKU in the filename to Chapter).

To make it easy to spread these repositories around, I have uploaded the scripts I used to generate the repository to google code (iphoneebookrepo). If you would like to help, either with code, by reporting faults (thanks Ross!), or just general comments, I would appreciate it!

Take that Kindle!

Wednesday, March 26, 2008

AppTapp Exec bug

I released my project to a couple of friendly users. After giving it a try, one of the testers came back and told me the installer was leaving crufty directories lying around in the root directory.

I eventually tracked it down to this:


<array>
   <string>Exec</string>
   <string>/bin/mkdir -p "/var/root/Media/EBooks/Beyond World's End"</string>
</array>

Now, if this was in a shell, or passed to the OS using "system" or a similar call, it would work. However, it seems that they are using direct calls to exec. I can't really tell, because AppTapp is oddly closed source.

The directories it was creating were:

  • "/var/root/Media/EBooks/Beyond
  • /World's
  • /End"

Fun!

Next up, I tried escaping the spaces, again with no luck. It looks like the code is splitting the parameters out through the use of spaces and ignores any attempt at grouping the packager might make.

Luckily CopyPath does the right thing, properly handling the spaces. It even creates parent directories, which is why I was using mkdir in the first place.

Now to figure out how to host it on Amazon S3. I really don't want a 2.5meg file being slurped across my cable modem every day!

Tuesday, March 25, 2008

Debugging an iPhone repository

The AppTapp Installer doesn't provide any feedback during a failed install or source update. This makes it very difficult to create packages. At first, it really feels like you are stumbling around in the dark.

Here is what I learned from my project.

Installer.app ignores bad XML documents. If you have one good version of your repository, and then make a change that results in a "bad" one, you will still see the good copy that has been cached on the phone. This confused me for a long time.

Always test the document for correctness first. Firefox is a quick, easy test tool. Point it at the URL, and if it displays the XML in tree format, you at least know that your XML is balanced, and that your web server is configured correctly.

Next, run Installer.app manually on the iPhone. This should let you know what is going wrong with the package. (original source)

  1. make sure you are not running Installer.app
  2. ssh into your iPhone (root password is "alpine")
  3. for 1.1.3 firmware and higher: su - mobile
  4. maximize your ssh window
  5. cd /Applications/Installer.app
  6. ./Installer
  7. you will see Installer.app pop up on the iPhone's screen
  8. reproduce the error
  9. observe the debugging output from Installer.app in the ssh window

Finally, never, ever change the root password on your iPhone. You may think that because you are running an SSH server that is open to the world that it would be a good idea to change it. After all, there is a "passwd" command sitting right there in the terminal window. RESIST!

There is a problem with the 1.1.3 and later firmwares, and the version of passwd shipped with the BSD subsystem results in Springboard (the desktop shell) crashing continuously. I made this mistake and had to completely wipe the phone and restore it.

For security, install the services application, and disable the SSH server between uses. This will also help with the battery life of the phone!

Creating an iPhone AppTapp Repository.

I had a project where I needed to create a repository for AppTapp (Installer.app), the application used for over the air software installation on jailbroken iphones.

It's near impossible to find instructions on how to do this. If you google for them, all you get are "how to jailbreak your phone" and "how do I install applications" questions. Nothing about how to actually create a repository!

Thankfully, I eventually found the a wiki on ipodtouchfans with a detailed description.

The repository file is an XML document using Apple's plist format. Since it's XML it is both easy to pick up and nasty to use at the same time. The header portion of the plist file is self-explanatory, check out the wiki link. Here's the header from my test repository.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>info</key>
 <dict>
  <key>name</key>
  <string>Testing</string>
  <key>maintainer</key>
  <string>Jason Pollock</string>
  <key>contact</key>
  <string>jason@pollock.ca</string>
  <key>url</key>
  <string>http://www.pollock.ca/repos/baen</string>
  <key>category</key>
  <string>TestRepositories</string>
 </dict>
 <key>packages</key>
 <array>
        </array>
</dict>
</plist>

The individual package instructions go in the final array. However, this is a fully specified (if empty) repository. If you place it on a publicly accessible URL, people will be able to add it to Installer.app and track updates!

Next are the individual packages and installation scripts. The package themselves are separated into scripts and .zip archives. There is no provision to deliver software in any other format.

The install/uninstall scripts are part of the repository XML, not the .zip archive. This means that you can host the .zip files on a cheap, low cost volume server, while the xml document (since it is generally smaller) can be hosted closer to you. Web servers are more able to cache the repository document. Hopefully, with a single http request, the application is able to tell from the header response if the repository has changed, keeping transfers to a minimum. To keep from having someone change the .zip file on you, both a size and an md5 hash are provided in the plist file. Finally, the separation between script and installed files allows other people to provide fixed installers for your application!

The install/uninstall scripts are also XML, complete with if/then/else syntax. This is near impossible to either understand or get right due to all of the tag nesting, and chaining of siblings without explicit keyword enclosures. For example, let's look at an extremely simple "if" block to check the firmware version.

First, the logic in C, just to let you see what it should be doing:


   if (! FirmwareVersionIs("1.1.2")) {
       AbortOperation("Firmware 1.1.2 is required to update to 1.1.3.");
   }

Now for the insanely verbose XML code.


           <array>
             <string>IfNot</string>
             <array>
               <array>
                 <string>FirmwareVersionIs</string>
                 <array>
                   <string>1.1.2</string>
                 </array>
               </array>
             </array>
             <array>
               <array>
                 <string>AbortOperation</string>
                 <string>Firmware 1.1.2 is required to update to 1.1.3.</string>
               </array>
             </array>
           </array>

Completely impenetrable.

Installer.app works best when the .zip is already in the destination layout. Then the installation process is relatively simple. However, if not, each file must be individually copied into its target location, using the <CopyPath> command.


<array>
    <string>CopyPath</string>
    <string>iZoo.app/</string>
    <string>/Applications/iZoo.app</string>
</array>

There are several gotchas.

First, if you create directories, Installer.app creates them with no permissions what so ever, you will need to perform a chmod!

Second, the location of the Media tree was moved between 1.1.2 and 1.1.3. In current releases of the iPhone firmware, applications run as the user mobile. This means that the Media tree (where the music/etc is stored), is now in /var/mobile/Media instead of /var/root/Media. Since applications are installed on both 1.1.2 and later versions of the firmware, they need to work on both. To help with this, Installer.app has added support for "~", to refer to the home directory for the user. Again, however, this depends on a specific version of Installer.app being present. Personally, I chose to ignore all of this, since the application I was using didn't know about /var/mobile/Media anyways. Still, it is something that developers will need to figure out.

Third the syntax in the scripts is nasty. If you find yourself writing a fair bit of it, write a tool to do it for you. I'm sure it is possible to come up with a more human readable syntax that will generate the XML for you.

Finally, debugging your packages is painful and slow. However, that's for the next post.

Other than that, there's nothing too difficult about the whole thing. If the zip file you are trying to install is already in the target layout, it's dead simple.

Enjoy!