Skip to content

Commit

Permalink
Add build instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
fxcoudert committed Nov 3, 2018
1 parent 3f5ba13 commit 9ee6f5e
Show file tree
Hide file tree
Showing 5 changed files with 468 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
The goal of this repository is to host GNU Fortran packages for macOS. These are simple installers, that will install the GCC compilers (including gfortran) in `/usr/local/gfortran`.

**[Follow this link to download!](https://github.com/fxcoudert/gfortran-for-macOS/releases)**

----

*If you are interested in how these installers are built, the documentation is [here](build_package.md).*
71 changes: 71 additions & 0 deletions build_package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# How to build a GCC installer for MacOS

These are my notes on how to build a gfortran / GCC installer for macOS. The goal is to obtain a nice Apple installer, that puts a complete GCC installation in `/usr/local/gfortran`. It then symlinks `gfortran` into `/usr/local/bin`.

The focus is on `gfortran`, as the Fortran community is a heavy user, because Apple's Xcode includes a nice C compiler (`clang`), but no Fortran compiler.

----

To build a package :

- I work in a work directory called `$ROOT`
- I have static GMP, MPFR, MPC and ISL libraries in `$ROOT/deps`. These can be take from the respective Homebrew packages, removing the shared libraries (`*.dylib`).

The steps are the following:

1. Edit `PATH` to make sure I don't have custom software (Homebrew, Anaconda, texlive, `/opt`, etc.)

2. Get the GCC sources (e.g. `gcc-8.2.0.tar.xz`), extract them to `$ROOT/gcc-8.2.0`. Create a `$ROOT/build` and go there.

3. Configure the build:

```
../gcc-8.2.0/configure --prefix=/usr/local/gfortran --with-gmp=$ROOT/deps --enable-languages=c,c++,fortran,objc,obj-c++ --build=x86_64-apple-darwin16
```

On Mojave and later, the following flags need to be added:

```
--disable-multilib --with-native-system-header-dir=/usr/include --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
```

4. Build by running `make`.

5. Check that the build worked by running `make check-gcc` and `make check-gfortran` inside `$ROOT/build/gcc`. (Or run `make check` at the toplevel, but it needs additional dependencies, such as `autogen`.)

6. Install with: `DESTDIR=$ROOT/package make install`

7. Strip some binaries, remove some hard links:
```
cd $ROOT/package/usr/local/gfortran
rm bin/*-apple-darwin* bin/c++
strip bin/*
strip libexec/gcc/*/*/*1* libexec/gcc/*/*/*2
strip libexec/gcc/*/*/install-tools/fixincl
```

8. Build a signed installer:
```
cd $ROOT
pkgbuild --root package --scripts package_resources --identifier com.gnu.gfortran --version 8.2.0 --install-location / --sign "Developer ID Installer: Francois-Xavier Coudert" gfortran.pkg
```

This requires a `postinstall` script inside `package_resources/`.

9. Verify the signature: `spctl --assess --type install gfortran.pkg`, which should exit with status code 0.

10. Create a DMG for the installer:
```
mkdir gfortran-8.2-Mojave
mv gfortran.pkg gfortran-8.2-Mojave/
cp package-README.html gfortran-8.2-Mojave/README.html
./mkdmg.pl gfortran-8.2-Mojave.dmg gfortran-8.2-Mojave/
```

During that step, look at the `README.html` file and update it if necessary!

11. Cleanup!
```
rm -rf gfortran-8.2-Mojave/
rm -rf package/*/
```
315 changes: 315 additions & 0 deletions mkdmg.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
#!/usr/bin/perl -w
#
# mkdmg.pl
# Version 1.0
# $Revision: 1.17 $
# $Date: 2004/02/05 02:14:39 $
#
# Copyright 2002 Evan Jones <[email protected]>
# http://www.eng.uwaterloo.ca/~ejones/
#
# Released under the BSD Licence, but I would appreciate it if you email
# me if you find this script useful, or if you find any bugs I should fix.
#
# Permission to use, copy, modify, distribute, and sell this software and its
# documentation for any purpose is hereby granted without fee, provided that
# the above copyright notice appear in all copies and that both that
# copyright notice and this permission notice appear in supporting
# documentation. No representations are made about the suitability of this
# software for any purpose. It is provided "as is" without express or
# implied warranty.
#
# Inspired by createDiskImage:
# http://graphics.stepwise.com/Articles/Technical/ProjectBuilder-DMG-Scripts.dmg
#
# Contributions:
# Ben Hines <[email protected]> - File names with periods and spaces
#
# TODO: Handle spaces in script mode.

use strict;

# Parse command line options
use Getopt::Std;

# TOOL CONFIGURATION
# Customize the tools that are used as you wish
# Note: These are all arrays to avoid undesired shell expansion

# Program that performs copies. You may prefer CpMac.
# If you want to use UFS, you need to run "cp" as root (I suggest using sudo)
my @copy = qw{cp};
my @copyOptions = qw{-R};

# Command to use to expand shell metacharacters: A string because we WANT to use the shell
my $expand = "/bin/ls -d";
sub shellExpandFileName( $ )
{
# expands the parameter using ls and the shell
my $arg = shift();
return split( ' ', `$expand $arg 2> /dev/null` );
}

# Create disk images
my @createImage = qw{hdiutil create};
my @createImageOptions = qw{-megabytes 200 -type SPARSE -fs HFS+ -quiet -volname};

# Mounting disk images
my $mountImage = "hdid";

# Unmounting disk images
my @unmountImage = qw{hdiutil eject};
my @unmountImageOptions = qw{-quiet};

# Converting disk images
my @convertImage = qw{hdiutil convert};
#my @convertImageOptions = qw{-format UDZO -imagekey zlib-level=9 -quiet -o};
my @convertImageOptions = qw{-format UDRO -quiet -o};

# Compressing disk images
my %compressCommands = (
'.dmg.bz2' => [ qw{nice bzip2 --best --force} ],
'.dmg.gz' => [ qw{nice gzip --best --force} ],
'.dmg' => undef,
);
my $compressCommand = undef;
my $compressedImage = undef;

# BEGIN PROGRAM

my %commandLineSwitches;
my $getoptsSucceeded = getopts( 'sv', \%commandLineSwitches );

my $verbose = exists( $commandLineSwitches{v} );
my $scriptMode = exists( $commandLineSwitches{s} );

# Validate command line options
if ( ! $getoptsSucceeded || ( $scriptMode && @ARGV < 1 ) || ( ! $scriptMode && @ARGV < 2 ) )
{
print <<EOF;
usage: mkdmg.pl [OPTION]... <ImageName> [FILE]...
-v Verbose: Prints messages about each step
-s Script mode: Reads source/destination from standard input
EOF
exit( 0 );
}

# Grab the size and image name arguments.
my $readonlyImage = shift();
my $specifiedImage = $readonlyImage;

# Match a compression format based on extension
$compressedImage = undef;
while ( my ($extension, $command) = each %compressCommands )
{
my $re = $extension;
$re =~ s/\./\\./;
if ( $readonlyImage =~ /^(.*)$re$/ )
{
$readonlyImage = $1;
$compressCommand = $compressCommands{$extension};
$compressedImage = "$readonlyImage$extension";
}
}

if ( ! defined $compressedImage )
{
# We must have a "compressed image" name, or else no extension (default compressed image)
fail( "Unable to determine output file type based on the extension: $2" ) if ( $readonlyImage =~ /^(.*)(\.[^.]*)$/ );

$compressCommand = $compressCommands{".dmg.bz2"};
$compressedImage = "$readonlyImage.dmg.bz2";
}

fail( "Zero length file name: Please specify a name without extension" ) if ( length $readonlyImage == 0 );

$readonlyImage .= ".dmg"; # Make sure it ends in .dmg

# Other file names and paths: based on the read-only image name
my $sparseImage = "$readonlyImage.sparseimage";
my $volumeName = $readonlyImage;
$volumeName =~ s/\.dmg$//;
my $mountPath = "/Volumes/$volumeName";

# Don't overwrite temporary files or mount when a volume with the same name is mounted
my @createdFiles = ( $sparseImage, $readonlyImage );
push( @createdFiles, $compressedImage ) if ( defined $compressCommand );
foreach my $file ( @createdFiles, $mountPath )
{
# If the file is specified on the command line, it is okay to clobber it
if ( $file ne $specifiedImage && -e $file )
{
print( "error: $file already exists\n" );
exit( 1 );
}
}
# Since we haven't created any files yet, don't put any on the list
@createdFiles = ();

# Find all the files we are going to copy
my @source;
my @destination;
if ( $scriptMode )
{
# Split input on white space
my @values = ();
while ( <> )
{
push( @values, split( ' ', $_ ) );
}

my $input = 1;
foreach my $value ( @values )
{
if ( $input )
{
push( @source, $value );
$input = 0;
}
else
{
push( @destination, $value );
$input = 1;
}
}

# There is an error if the number of source files does not match the number of destination files
fail( "Uneven number of input lines: A source file does not have a destination" ) if ( @source != @destination );
}
else
{
# For command line mode we do no renaming or shell expansion: Just do it as is
foreach my $line ( @ARGV )
{
push( @source, $line );
push( @destination, "" );
}
}

# Verify that we have input
fail( "No input files specified" ) if ( ! @source );

# Verify that we can find all the source files
foreach my $sourceFile ( @source )
{
my @expandedSource = ( $sourceFile );
# If we are in script mode, expand shell characters
if ( $scriptMode )
{
@expandedSource = shellExpandFileName( $sourceFile );
fail( "Files not found: $sourceFile" ) if ( @expandedSource == 0 );
}

foreach my $file ( @expandedSource )
{
fail( "Cannot not find: $file (matched $sourceFile)" ) if ( ! -e $file );
fail( "Cannot read: $file (matched $sourceFile)" ) if ( ! -r $file );
}
print "$sourceFile matches " . join( ", ", @expandedSource ) . "\n" if ( $verbose );
}

# Create the image and format it
print( "Creating temporary disk image: $sparseImage...\n" ) if ( $verbose );
push( @createdFiles, $sparseImage );
my $code = system( @createImage, $readonlyImage, @createImageOptions, $volumeName );
fail( "Creating temporary disk image failed" ) if ( $code || ! -e $sparseImage );

# Mount the disk image
print( "Mounting temporary disk image: $sparseImage...\n" ) if ( $verbose );
my $output = `$mountImage '$sparseImage'`;
fail( "Disk image not mounted at $mountPath" ) if ( ! -e $mountPath );

fail( "Could not determine mount device" ) if ( $output !~ m{^(/dev/disk\d+)\s+\S+\s*$}m );
my $mountDevice = $1;

# Copy files to the disk image
print( "Copying files...\n" ) if ( $verbose );

$code = 0;
for ( my $i = 0; $i < @source && ! $code; ++ $i )
{
my @expandedSource = ( $source[$i] );
# If we are in script mode, expand shell characters
@expandedSource = shellExpandFileName( $source[$i] ) if ( $scriptMode );

my $destDir = ".";
# If the source expands to multiple items, and a destination is specified,
# the destination is a directory to be created.
if ( @expandedSource > 1 && length $destination[$i] )
{
$destDir = $destination[$i];
}
# If the source is a single file and the destination contains slashes, make sure that each
# directory before the file name is created.
elsif ( $destination[$i] =~ m{(.*)/[^/]*$} )
{
$destDir = $1;
}

if ( ! -e "$mountPath/$destDir" )
{
print( "Creating destination directory: $mountPath/$destDir\n" ) if ( $verbose );

# Split the directory into seperate parts ( dir1/dir2/dir3 => dir1, dir2, dir3 )
my @hierarchy = split( /\//, $destDir );
$destDir = "";
foreach my $subdirectory ( @hierarchy )
{
$destDir .= "/$subdirectory";
mkdir( "$mountPath$destDir" ) or fail( "Could not create directory: $mountPath$destDir: $!" ) if ( ! -e "$mountPath$destDir" );
}
}

$code = system( @copy, @copyOptions, @expandedSource, "$mountPath/$destination[$i]" );
fail( "Files did not copy successfully" ) if ( $code );
}

# Unmount the disk image
print( "Unmounting temporary disk image: $sparseImage...\n" ) if ( $verbose );
$code = system( @unmountImage, $mountDevice, @unmountImageOptions );
fail( "Unmounting disk image failed" ) if ( $code or -e $mountPath );

# Create the read-only image
# Formats (from largest to smallest):
# UDRO - Read only uncompressed
# UDCO - ADC compressed
# UDZO - Zlib compress (specify -imagekey zlib-level=9)
# UDRO.gz - Read only gzip compressed
# UDRO.bz2 - Read only bzip2 compressed (BEST)
print( "Creating read only disk image: $readonlyImage\n" ) if ( $verbose );
push( @createdFiles, $readonlyImage );
system( @convertImage, $sparseImage, @convertImageOptions, $readonlyImage );
unlink( $sparseImage );
unlink( "._$sparseImage" ) if ( -e "._$sparseImage" ); # Remove "resource fork" on any non HFS file systems
fail( "Read only image: $readonlyImage does not exist" ) if ( ! -e $readonlyImage );
unlink( "._$readonlyImage" ) or die "Could not unlink: $!" if ( -e "._$readonlyImage" ); # Remove "resource fork" on any non HFS file systems

print( "Read only disk image sucessfully created: $readonlyImage\n" ) if ( $verbose );

# Compress the disk image
if ( defined $compressCommand )
{
print( "Compressing disk image: $readonlyImage to $compressedImage\n" ) if ( $verbose );
push( @createdFiles, $compressedImage );
$code = system( @$compressCommand, $readonlyImage );
fail( "Compressing image failed" ) if ( $code or ! -e $compressedImage );

print( "Compressed image sucessfully created: $compressedImage\n" ) if ( $verbose );
}

# Cleans up and quits in a gross fashion
sub fail
{
my $string = shift();
print( "error: $string\n" );

# Gross hack: We exploit global variables to try and clean up gracefully
system( @unmountImage, $mountDevice, @unmountImageOptions ) if ( defined $mountDevice );
foreach my $file ( @createdFiles )
{
unlink $file if ( -e $file );
unlink( "._$file" ) if ( -e "._$file" ); # Remove "resource fork" on any non HFS file systems
}

exit( 1 );
}
Loading

0 comments on commit 9ee6f5e

Please sign in to comment.