#!/usr/bin/perl # # Perl script to export a product. # # Created by Geoffrey Schmit on 1/1/2007. # Copyright (c) 2007 Sugar Maple Software, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software # and associated documentation files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom # the Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all copies or # substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING # BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # # This script integrates with the Perforce SCM. If you are using another SCM, you will need to # modify this script. # # This script assumes that Perforce change descriptions use the following tags: # # A new feature that was added as part of this submission. # # A behavior change that was made as part of this submission. # # A bug that was fixed as part of this submission. # The text after these tags until the next newline will be included in the release notes. # All other text in the change description is ignored. # # This script assumes that at least a shell of a release notes HTML file has already been # created. # # This script assumes that at least a shell of an AppCast XML file has already been created and # contains the "" comment to specify where additional releases # should be inserted. # # This script assumes the following export directory structure: # export # . (1.0) # (1.0.0b1) # notes.html # Release # .dmg # Debug # .dmg # use Getopt::Long; use strict; use warnings; use POSIX qw(strftime); use MacPerl 'DoAppleScript'; use File::Temp qw( tempdir ); use IPC::Open2; my $kProductName = "NewsHawker"; my $kAppCastXMLFile = lc( $kProductName ) . ".xml"; my $kCreditsFile = "Resources/Credits.html"; my $kDownloadURL = "http://sugarmaplesoftware.com/releases/" . lc( $kProductName ); my $kDMGTemplate = "template.sparseimage"; my $kExportPath = "../../export"; my $kLicenseFile = "license.html"; my $kReleaseNotesFile = "notes.html"; my $kSourceFile = "Source/${kProductName}Delegate.m"; my $kXcodeApplicationVersionBuildSetting = "APPLICATION_VERSION"; my $kXcodeProject = "$kProductName.xcodeproj"; my $kXcodeReleaseBuildConfiguration = "Release"; my $kXcodeTarget = $kProductName; my $gP4Options = ""; my %gOptions = (); my $gHelpStr = <, -p ). See p4 help usage for complete list. EOT ; # main { $Getopt::Long::ignorecase = 0; &GetOptions( \%gOptions, "c=s", "d=s", "H=s", "p=s", "P=s", "u=s", "help|h|?" ) or die( "Error parsing command-line options: $!" ); if( $gOptions{"help"} || @ARGV > 0 ) { print $gHelpStr; exit; } # capture the p4 options $gP4Options .= " -c $gOptions{'c'}" if $gOptions{"c"}; $gP4Options .= " -d $gOptions{'d'}" if $gOptions{"d"}; $gP4Options .= " -H $gOptions{'H'}" if $gOptions{"H"}; $gP4Options .= " -p $gOptions{'p'}" if $gOptions{"p"}; $gP4Options .= " -P $gOptions{'P'}" if $gOptions{"P"}; $gP4Options .= " -u $gOptions{'u'}" if $gOptions{"u"}; # open the Xcode project my $status = system( "open ./$kXcodeProject" ); die "open failed: $?" if $status; # get the version number specified in the project my $version = getVersionNumber(); print "Exporting $version...\n"; # check for opened files my $openedFiles = p4Command( "opened ..." ); die "Files are opened: $openedFiles" unless ( $openedFiles =~ m@file\(s\) not opened on this client\.@ ); createExportDirectories( $version ); # if this is a pre-release export, update when it expires $version =~ m@^(\d+)\.(\d+)\.(\d+)([dabf])(\d+)$@ or die "invalid version: $version"; if( $4 ne "f" ) { updatePreReleaseExpirationDate(); } buildProduct( "Release" ); buildProduct( "Debug" ); updateReleaseNotes( $version ); buildDMG( $version, "Release" ); buildDMG( $version, "Debug" ); updateAppCastXML( $version ); # increment the version number specified in the project so that any future builds will not # have the same version number as this export incrementVersionNumber( $version ); print "After verification, submit the pending changes.\n"; } sub getVersionNumber { my $versionQueryAppleScript = <&1" ); print $writeFH $changeDescription; close( $writeFH ); my $p4Output = <$readFH>; close( $readFH ); waitpid( $pid, 0 ); } sub buildProduct { my $buildConfiguration = shift; print "Building $buildConfiguration build...\n"; # invoke xcodebuild and redirect stderr to stdout so that we can capture failures my $output = `xcodebuild -alltargets -configuration $buildConfiguration clean build 2>&1`; die "Build failed:\n$output" unless( $output =~ m@\*\* BUILD SUCCEEDED \*\*@s ); } sub updateReleaseNotes { print "Updating release notes...\n"; my $version = shift; my ( $majorVersion, $minorVersion ) = extractMajorAndMinorVersions( $version ); p4Command( "edit $kReleaseNotesFile" ); my $fileContents = readFile( $kReleaseNotesFile ); # capture the latest change, if any, reflected in the release notes my $lastChangeInReleaseNotes = 0; if( $fileContents =~ m@@s ) { $lastChangeInReleaseNotes = $1; } my $mostRecentChange = 0; my @features = (); my @changes = (); my @fixes = (); # get all of the change numbers that have been submitted for this product; they will be # returned in order from newest to oldest my @p4Changes = split /\n/, p4Command( "changes ..." ); foreach my $p4Change ( @p4Changes ) { if( $p4Change =~ m@^Change (\d+)@ ) { my $changeNumber = $1; # cache the latest change number submitted for this product if( $mostRecentChange == 0 ) { $mostRecentChange = $changeNumber; } # if the release notes already reflect this change, we're done if( $changeNumber <= $lastChangeInReleaseNotes ) { last; } my @changeDescriptions = split /\n/, p4Command( "describe -s $changeNumber" ); foreach my $changeDescription ( @changeDescriptions ) { # if we have made it to this part of the change description, there are no more # comments to parse if( $changeDescription =~ m@^Affected files \.\.\.@ ) { last; } elsif( $changeDescription =~ m@^\s+(.+)$@ ) { push @features, $1; } elsif( $changeDescription =~ m@^\s+(.+)$@ ) { push @changes, $1; } elsif( $changeDescription =~ m@^\s+(.+)$@ ) { push @fixes, $1; } } } } # construct the HTML that we will insert into the existing release notes. my $releaseNotesAddition = <

$kProductName $version

EOT ; if( @features ) { $releaseNotesAddition .= "

New Features

\n
    \n"; foreach my $feature ( @features ) { $releaseNotesAddition .= "
  • $feature
  • \n"; } $releaseNotesAddition .= "
\n"; } if( @changes ) { $releaseNotesAddition .= "

Behavior Changes

\n
    \n"; foreach my $change ( @changes ) { $releaseNotesAddition .= "
  • $change
  • \n"; } $releaseNotesAddition .= "
\n"; } if( @fixes ) { $releaseNotesAddition .= "

Fixes

\n
    \n"; foreach my $fix ( @fixes ) { $releaseNotesAddition .= "
  • $fix
  • \n"; } $releaseNotesAddition .= "
\n"; } $fileContents =~ s@@$releaseNotesAddition@s; writeFile( $kReleaseNotesFile, $fileContents ); # copy the updated release notes to the export directory my $status = system( "cp $kReleaseNotesFile " . "$kExportPath/$majorVersion.$minorVersion/$version/$kReleaseNotesFile" ); die "cp failed: $?" if $status; } sub buildDMG { # # This subroutine is based on a shell script provided by Jonathan Wight on #macsb # my $version = shift; my $buildConfiguration = shift; print "Building DMG for $buildConfiguration...\n"; my ( $majorVersion, $minorVersion ) = extractMajorAndMinorVersions( $version ); # create the temporary directory my $tempDirectory = tempdir( "/tmp/TMPXXXX" ); # create the mount point my $mountPoint = "$tempDirectory/mountpoint"; mkdir $mountPoint; # copy the template disk image my $status = system( "cp $kDMGTemplate $tempDirectory" ); die "cp failed: $?" if $status; # mount the disk image $status = system( "hdiutil attach $tempDirectory/$kDMGTemplate -private -mountpoint " . "$mountPoint -quiet" ); die "hdiutil attach failed: $?" if $status; # remove the placeholder files $status = system( "rm -rf $mountPoint/$kProductName.app" ); die "rm failed: $?" if $status; $status = system( "rm -f $mountPoint/$kLicenseFile" ); die "rm failed: $?" if $status; $status = system( "rm -f $mountPoint/$kReleaseNotesFile" ); die "rm failed: $?" if $status; # copy the files to disk image $status = system( "cp -R build/$buildConfiguration/$kProductName.app $mountPoint" ); die "cp -R failed: $?" if $status; $status = system( "cp $kLicenseFile $mountPoint" ); die "cp -R failed: $?" if $status; $status = system( "cp $kReleaseNotesFile $mountPoint/$kReleaseNotesFile" ); die "cp -R failed: $?" if $status; # strip the application $status = system( "strip $mountPoint/$kProductName.app/Contents/MacOS/*" ); die "strip failed: $?" if $status; # unount the disk image $status = system( "hdiutil detach $mountPoint -quiet" ); die "hdiutil detach failed: $?" if $status; # convert the disk image $status = system( "hdiutil convert $tempDirectory/$kDMGTemplate -format UDZO -o " . "$kExportPath/$majorVersion.$minorVersion/$version/$buildConfiguration/$kProductName.dmg " . "-quiet" ); die "hdiutil convert failed: $?" if $status; } sub updateAppCastXML { print "Updating AppCast XML...\n"; my $version = shift; my ( $majorVersion, $minorVersion ) = extractMajorAndMinorVersions( $version ); p4Command( "edit $kAppCastXMLFile" ); my $fileContents = readFile( $kAppCastXMLFile ); my $dmgSize = -s "$kExportPath/$majorVersion.$minorVersion/$version/Release/$kProductName.dmg"; my $timeString = strftime "%a, %d %b %Y %T %z", localtime; my $versionedURL = "$kDownloadURL/$majorVersion.$minorVersion/$version"; my $newItem = < Version $version $versionedURL/$kReleaseNotesFile $timeString EOT ; $fileContents =~ s@@$newItem@s; writeFile( $kAppCastXMLFile, $fileContents ); } sub incrementVersionNumber { my $version = shift; print "Incrementing version number...\n"; # open the Xcode project for edit p4Command( "edit $kXcodeProject/..." ); # open the Xcode project my $status = system( "open ./$kXcodeProject" ); die "open failed: $?" if $status; # increment the version number specified by the specified setting in all build configurations # of the specified target. my $newVersion = $version; if( $newVersion =~ m@^(\d+)\.(\d+)\.(\d+)([dabf])(\d+)$@ ) { my $newBuild = $5 + 1; $newVersion = "\"$1.$2.$3$4$newBuild\""; } my $versionSetAppleScript = <; close FILE; $/ = $tempDef; return $fileContents; } sub writeFile { my $fileName = shift; my $fileContents = shift; open FILE, ">$fileName" or die "Can not open: $fileName for writing $!"; print FILE $fileContents; close FILE; } sub p4Command { my $command = shift; my $p4Output = `p4 $gP4Options $command 2>&1`; die "p4Command: $command failed: ($?)\n$p4Output" if $?; return $p4Output; }