From 9ee6f5e584a64514575e0caafc9f61c0df407b20 Mon Sep 17 00:00:00 2001 From: FX Coudert Date: Sat, 3 Nov 2018 14:32:43 +0100 Subject: [PATCH] Add build instructions --- README.md | 4 + build_package.md | 71 ++++++++ mkdmg.pl | 315 ++++++++++++++++++++++++++++++++++ package-README.html | 73 ++++++++ package_resources/postinstall | 5 + 5 files changed, 468 insertions(+) create mode 100644 build_package.md create mode 100755 mkdmg.pl create mode 100644 package-README.html create mode 100755 package_resources/postinstall diff --git a/README.md b/README.md index 344a0f7..5ceb25d 100644 --- a/README.md +++ b/README.md @@ -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).* diff --git a/build_package.md b/build_package.md new file mode 100644 index 0000000..e2f2bdb --- /dev/null +++ b/build_package.md @@ -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/*/ +``` diff --git a/mkdmg.pl b/mkdmg.pl new file mode 100755 index 0000000..40176f0 --- /dev/null +++ b/mkdmg.pl @@ -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 +# 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 - 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 < [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 ); +} diff --git a/package-README.html b/package-README.html new file mode 100644 index 0000000..1daaf98 --- /dev/null +++ b/package-README.html @@ -0,0 +1,73 @@ + + + + + How to install gfortran for Mac + + + + + + + + + +

How to install gfortran for Mac

+ +
    +
  1. Install Xcode +
      +
    • Install the Xcode application from the Mac App Store
    • +
    • Open the installed Xcode app, agree to the license, and let it install some "compoments"
    • +
    +
  2. + +
  3. Install the Xcode command-line tools +
      +
    • Open a terminal window, by clicking on this link or opening the Terminal application (Applications > Utilities > Terminal)
    • +
    • Type xcode-select --install and follow the dialogs that open
    • +
    +
  4. + +
  5. Install gfortran itself +
      +
    • Within this package, run the gfortran installer, follow the dialogs
    • +
    +
  6. + +
  7. Enjoy! +
      +
    • You now have a full gfortran (in fact, a full GCC) installed in /usr/local/gfortran
    • +
    • In the terminal, you simply type "gfortran" to run the compiler
    • +
    +
  8. +
+ + +

Where to find help

+ + + + + + + diff --git a/package_resources/postinstall b/package_resources/postinstall new file mode 100755 index 0000000..af53819 --- /dev/null +++ b/package_resources/postinstall @@ -0,0 +1,5 @@ +#!/bin/sh + +test -d /usr/local/bin || mkdir -p /usr/local/bin +test -e /usr/local/bin/gfortran && rm /usr/local/bin/gfortran +ln -s /usr/local/gfortran/bin/gfortran /usr/local/bin/gfortran