Wednesday, March 14, 2018

Staged Building

OUT OF DATE
Now and then since last year I've been trying to improve my skill on building myself some more up-to-date 64-bits software artifacts to use on my prefered OS (currently, Solaris 11.3 GA) and optimized to my particular CPU (an old Intel Core 2). I've been somewhat radical experimenting this for a few applications, programming languages and GNU tools and utilities. The learning curve is slow and rather steep, with the added difficulty that a Solaris system isn't a true GNU/Linux, despite the honorable efforts to make it compatible enough for running some popular GNU software and performing standard GNU builds of open-source software.


Neither the previous posts are fully satisfying to me, but I could explore and evolve with each of them. Some open-source software is fully compatible with the GNU building system. Others are at most partially compatible. And yet others aren't compatible at all. Few software offer a certain degree of compatibility with native compilers instead of GNU GCC. In general, by using GCC one increases the chances of a successful build. But again and in many cases, Solaris doesn't provide a sufficiently up-to-date GNU GCC compiler, which, in most cases, set a tombstone for the project. A naive thinking could argue that it would suffice to build a very updated (if not state-of-the-art) GNU GCC before attempting any other build. But following this approach one quickly realizes that a lot dependencies must be built beforehand and this can only be done with the outdated tools currently available.

Another difficulty is packaging the resulting software artifacts so that it can be more easily managed throughout other instances of the OS. The ultimate solution is building an IPS package. Although powerful and very desirable on a daily basis, it's not that trivial to build a good IPS package. There are lots of details involved as described on the man page pkg(5). Furthermore, there are other related techniques which are necessary to better integrate, organize and manage the building and distribution of software: a few of them I could grasp on the Oracle Solaris How-To Articles. The general workflow is frequently mixed with some particular case, which sometimes causes some confusion.

Because of all these difficulties my blog entries reveals my hurdles and evolution towards learning the best, I mean, the simplest and most resilient (robust), way (architecture) which takes advantage of the greatest Solaris features for building and running popular open-source software.

At the present time I'd like to think I was able to get to a reasonable start, which was sort of summarized in GNU - Build preparation. But looking forward to IPS packaging in subsequent yet to come blog posts I suspect it has a flaw related to the designated IPS staging-area (/software/prototype/...) and the GNU build-configuration (--prefix=...). Up to now I was setting --prefix=/software/prototype/..., but instead I now realize I should:
  1. Set --prefix to where I intend to be the final resting place of the software artifacts. For instance, somewhere under /opt. Of course, these shall not be the IPS staging-area.
     
  2. Set the DESTDIR environment variable according to the GNU Coding Standards on the same line of the install command. (sudo gmake DESTDIR=/stage/prototype/... install).
      
  3. Carefully assess an appropriate RLE configuration. This means setting the LD_CONFIG_32 or LD_CONFIG_64 environment variable or even the default system configuration.
Hence:

I must review part of the gnu-build-preparation script I presented earlier on GNU - Build preparation, performing the following changes:

  • Replacing every occurrence of DS_SOFTWARE with DS_STAGE.
  • Replacing every occurennce of  /software with /stage.

I must review the general building workflow somewhat as follows:
(assuming 64-bits  build and final resting place somewhere under /opt)

  • ./configure --prefix=/opt/... ...
  • gmake -j$(( $(psrinfo |wc -l) - 1 ))
  • sudo gmake DESTDIR=/stage/prototype/.../64 install

I must prepend via an appropriate RLE configuration the following directories (in my particular case) to the default (64-bits in general) library search paths:

  • /opt/gnu/lib
  • /opt/usr/lib

WARNING
A word of caution! When doing a staged build looking forward to build a system (IPS) package by using DESTDIR pointing to a prototype area in gmake install, it may be necessary to execute some post deployment actions which aren't possible when delivering to the prototype area. For instance, when building GNU libsigsegv you'll get a warning remembering to run "libtool --finish ${prefix}". This would have to be accomplished by some IPS "post-processing" rule.

I also take the opportunity to experiment with getting away with gnu32 and gnu64 in favor of just 32 and 64, adjusting the artifact subtree in prototype and hopefully better naming a few variables and functions. Similarly to the previous version, the revised script assumes the following ZFS delegations are already in place:

$ zfs allow rpool/stage/build
---- Permissions on rpool/stage/build ------------------
Permission sets:
    @descendent clone,compression,destroy,promote,quota,

                readonly,rename,reservation,share,sharenfs
    @generic create,diff,hold,mount,receive,

             release,rollback,send,snapshot,userprop
Descendent permissions:
    user user1 @descendent
Local+Descendent permissions:
    user user1 @generic


$ zfs allow rpool/stage/prototype
---- Permissions on rpool/stage/prototype --------------
Permission sets:
    @descendent clone,compression,destroy,promote,quota,

                readonly,rename,reservation,share,sharenfs
    @generic create,diff,hold,mount,receive,

             release,rollback,send,snapshot,userprop
Descendent permissions:
    user user1 @descendent
Local+Descendent permissions:
    user user1 @generic


For convenience, I list the revised gnu-build-preparation script below:

#!/bin/bash -
 

################################################################

if [[ -z "$1" ]];
then

  echo
  echo "Usage: $0 <tarball>"

  echo
  exit 1
else
  if ! [[ -f "$1" ]];
  then

    echo
    echo "$1 not found."

    echo
    exit 1
  fi
fi

  
################################################################

# This can be potentially better suited elsewhere
# as long as it doesn't break previous assumptions.

DS_BASE=rpool

DS_STAGE=${DS_BASE:+$DS_BASE/}stage
DS_BUILD=build
DS_PROTOTYPE=prototype


################################################################

#
# legacy gnu-coreutils IPS package lacks realpath
# TAR=$(realpath "$1")

TAR=$(readlink -f "$1") 
 
A=$(basename $1 |\
    sed 's/\(\w\+\([-.]\w\+\)*\)-.*/\1/')

V=$(basename $1 |\

    sed 's/\w\+\([-.]\w\+\)*-\([0-9]\+\([_.-][0-9]\+\)*[a-z]*\).*/\2/')

echo

echo Processing $TAR
echo
echo ------------------------------------
echo "App: $A"
echo "Ver: $V"
echo ------------------------------------

echo
echo In the process, the following ZFS datasets will be created:

echo
echo "  $DS_
STAGE/$DS_BUILD/$A"
echo "  $DS_
STAGE/$DS_BUILD/$A/$A-$V"
echo "  $DS_
STAGE/$DS_BUILD/$A/$A-$V-32"
echo "  $DS_
STAGE/$DS_BUILD/$A/$A-$V-64"
echo
echo "  $DS_
STAGE/$DS_PROTOTYPE/$A"
echo "  $DS_
STAGE/$DS_PROTOTYPE/$A/$A-$V"
echo "  $DS_
STAGE/$DS_PROTOTYPE/$A/$A-$V/32"
echo "  $DS_
STAGE/$DS_PROTOTYPE/$A/$A-$V/64"
echo
read -p "Enter \"y\" to proceed: " -r
if ! [[ "$REPLY" = "y" ]] && ! [[ "$REPLY" = "Y" ]];
then
  echo "Exiting..."
  exit 1
fi 

  
################################################################

function dataset-exists()
{
  zfs list -H "$1" >/dev/null 2>&1
  local rv=$?
  echo $rv


################################################################

function get-mountpoint()
{
  echo $(zfs get -H -o value mountpoint "$1" 2>/dev/null)


################################################################

function dataset-assert-mounted()
{
  if [[ -z $(get-mountpoint "$1") ]]; then
    echo "Unexpected: \"$1\" doesn't seem to be mounted."
    exit 1
  fi
}

dataset-assert-mounted "$DS_
STAGE"
dataset-assert-mounted "$DS_
STAGE/$DS_BUILD"
dataset-assert-mounted "$DS_
STAGE/$DS_PROTOTYPE" 

################################################################

function create-source-area()
{
  if [[ $(dataset-exists "$1") -eq 1 ]]; then
    echo "Creating $1..."
    zfs create "$1"
  else
    local MP=$(get-mountpoint "$1")
    if [[ $(basename "$MP") == "$A" ]]; then
      echo "No need to create $1."
    else
      echo
      echo "$1 mounted as $MP."
      echo "No further action can be safely taken."
      echo "Exiting..."
      exit 1
    fi
  fi
}

echo

   
create-source-area "$DS_STAGE/$DS_BUILD/$A"
create-source-area "$DS_STAGE/$DS_BUILD/$A/$A-$V"
 

################################################################

echo

DS_SOURCE="$DS_
STAGE/$DS_BUILD/$A"
MP_SOURCE=$(get-mountpoint "$DS_SOURCE")

DS_SOURCE_VERSION="$DS_SOURCE/$A-$V"
MP_SOURCE_VERSION=$(get-mountpoint "$DS_SOURCE_VERSION")

if [[ $(ls -A "$MP_SOURCE_VERSION" 2>/dev/null) ]]; then
  echo
  echo "Directory $MP_SOURCE_VERSION not empty!"
  echo "No further action can be safely taken."
  echo "Exiting..."
  exit 1
fi

(cd "$MP_SOURCE_VERSION/.."; gtar xf "$TAR")

if ! [[ $(ls -A "$MP_SOURCE_VERSION" 2>/dev/null) ]]; then
  echo
  echo "Directory $MP_SOURCE_VERSION shouldn't be empty!"
  echo "No further action can be safely taken."
  echo "Exiting..."
  exit 1
fi

zfs snapshot "$DS_SOURCE_VERSION@source"
zfs set readonly=on "$DS_SOURCE_VERSION" 

  
################################################################

function set-target()
{
  local T=$1
 

  # This may be inappropriate in some cases.
  # Some "packages" require/prefer just an empty dataset.

  zfs clone "$DS_SOURCE_VERSION"@source \

            "$DS_SOURCE_VERSION-$T"
  

  # This may be too soon.
  # The "package" may need some manual fix before @start

  zfs snapshot "$DS_SOURCE_VERSION-$T"@start
}

set-target 32
set-target 64


################################################################

zfs list -H -r -t all -o name "$DS_SOURCE"


################################################################

#
# Create the final resting place of the results.
# Use an independent ZFS pool/dataset, not rpool/VARSHARE.
# At end consider snapshoting, just in case.
# Could be a staging area for packaging.
#
 
 

DS_ARTIFACT="$DS_STAGE/$DS_PROTOTYPE/$A"
DS_ARTIFACT_VERSION="$DS_ARTIFACT/$V"

 
echo
echo "Creating "$DS_ARTIFACT" subtree."
echo

function create-staging-area()
{
  if [[ $(dataset-exists "$1") -eq 1 ]]; then
    zfs create -p "$1"
  fi
}

create-staging-area "$DS_ARTIFACT_VERSION"
create-staging-area "$DS_ARTIFACT_VERSION/32"
create-staging-area "$DS_ARTIFACT_VERSION/64"

zfs list -H -r -t all -o name "$DS_ARTIFACT"


MP_PROTOTYPE=$(get-mountpoint "$DS_STAGE/$DS_PROTOTYPE")
 
################################################################  

echo
echo "Creating pre-configuration script."

 
cat > "$MP_SOURCE/setenv-$V" <
<EOF

#
# HOSTTYPE=$HOSTTYPE
# OSTYPE=$OSTYPE
# MACHTYPE=$MACHTYPE
#
# Up to Solaris 11.3 the default is 32-bits.
# MACHTYPE is typically i386-pc-solaris2.11
#
# Since Solaris 11.4 Beta the default is 64-bits.
# MACHTYPE is x86_64-pc-solaris2.11
#
# It's advisable not to override the above env vars
# buy you experiment with the --build or --target options
# to the standard GNU's configuration script: configure.
# (they are important to properly organize the built artifacts)
#
# Bottom line it seems that the -m option to the flags
# CFLAGS, CXXFLAGS and LDFLAGS always prevail after all.
# (in terms of the actual bitness of the built artifacts)
#


if [[ -z "\$1" ]];
then
  echo "Usage: source setenv <bit-target>"
  return 1
else
  if [[ "\$1" -ne 32 ]] && [[ "\$1" -ne 64 ]];
  then
    echo "Valid bit-targets: 32 or 64."
    return 1
  fi
fi


# Despite setting bitness as shown next
# DON'T override HOSTTYPE and MACHTYPE
# Just remind the proper --build option


BITS=\$1

  
echo

function add-path()
{
  local COMPONENT="\$1"

  if [[ -d "\$COMPONENT" ]] ;
  then
    ! [[ "\$PATH" =~ "\$COMPONENT" ]] && \
      export PATH="\$COMPONENT:\$PATH"
  fi
}

function add-pkgconfig-path()
{
  local COMPONENT="\$1"
  local TAIL="\${PKG_CONFIG_PATH:+:\$PKG_CONFIG_PATH}"

  if [[ -d "\$COMPONENT" ]] ;
  then
    ! [[ "\$TAIL" =~ "\$COMPONENT" ]] && \
      export PKG_CONFIG_PATH="\$COMPONENT\$TAIL"
  fi
}

function extend-env()
{
  
# In this revision I'm only focusing on 64-bits.
  # Hence, I'm omitting the bitness subfolder.
  # local BASE="\$1/\$BITS" 
  local BASE="\$1"

  add-path "\$BASE/bin"
  add-pkgconfig-path "\$BASE/lib/pkgconfig"
}

add-path /usr/gnu/bin

 

#
# Other PATH and PKG_CONFIG_PATH settings.
# Put entries in reverse order of dependency.
# (following my PATH building suggestion)
# The samples below are pre-Solaris 11.4 Beta.
#

# ...
# extend-env /opt/app
# extend-env /opt/usr

# extend-env /opt/gnu
  
#
# RLE configuration
#


LIBBITS=\$([[ \$BITS -eq 32 ]] && echo "" || echo "/64")
RLEBITS=\$([[ \$BITS -eq 32 ]] && echo "" || echo "-64")
RLECFG="$MP_SOURCE/ld.config-\$BITS"


#
# /opt/gnu/lib and /opt/usr/lib are fixed for simplification here
# but could mimic the system /usr/gnu/lib and /usr/lib
#

 

crle \$RLEBITS \\
     -c "\$RLECFG" \\
     -l /opt/gnu/lib:/opt/usr/lib \\
     -l /lib\$LIBBITS:/usr/lib\$LIBBITS
 

crle \$RLEBITS -c "\$RLECFG"

export LD_CONFIG_\$BITS="\$RLECFG"
echo LD_CONFIG_\$BITS="\$RLECFG"
 

echo
 

# In general, not a good idea; evaluate.
# export CONFIG_SHELL=/bin/bash

echo CONFIG_SHELL=\$CONFIG_SHELL

echo
 

#
# The following compilers and linker overrides
# may be troublesome, specially the -m and -std options
# as it may be very source dependent; evaluate.
# The -std bar minimums on Solaris 11 Express is gnu89 and gnu++98.

# The -std bar minimums on Solaris 11.3 is gnu89 and gnu++03.
# On Solaris 11.4 Beta gnu11 and gnu++11 seem Ok.
#


FLAGS="-m\$BITS -march=core2 -mtune=core2"

export CC=/usr/bin/gcc
export CFLAGS="\$FLAGS -std=gnu89"
echo CC=\$CC CFLAGS=\$CFLAGS

echo

export CXX=/usr/bin/g++
export CXXFLAGS="\$FLAGS -std=gnu++03"
echo CXX=\$CXX CXXFLAGS=\$CXXFLAGS

echo

export LD=/usr/bin/ld

export LDFLAGS="\$FLAGS"
echo LD=\$LD LDFLAGS=\$LDFLAGS 

echo

echo PATH=\$PATH

echo

echo PKG_CONFIG_PATH=\$PKG_CONFIG_PATH

unset FLAGS
unset LIBBITS
unset RLEBITS
unset RLECFG


function get-target()
{
  case \$BITS in
  32)
    echo i386-pc-solaris2.11
    ;;
  64)
    echo x86_64-pc-solaris2.11
    ;;
  esac
 

}
 
echo
echo Suggested build sequence:
echo 
echo Fine-tune/fix config.h.in, Makefile.in and others...
echo 
echo \$ ./configure \\\\
echo "    "--build=\$(get-target \$BITS) \\\\
echo "    "--prefix=/opt/... \\\\
echo "    "...
echo
echo \$ gmake -j\$(( \$(psrinfo |wc -l) - 1 ))
echo 
echo \$ su
echo \# source ../setenv-$V \$BITS
echo 
echo For IPS package:
echo 
echo \# gmake DESTDIR=$MP_PROTOTYPE/$A/$V/\$BITS install
echo
echo For immediate use:
echo
echo \# gmake install echo \# zfs snapshot -r .../opt/...@$A-$V

EOF

cat > "$MP_SOURCE/unsetenv-$V" <<EOF

function cleanup-env()
{

  [[ -f "\$LD_CONFIG_32" ]] && rm "\$LD_CONFIG_32"
  unset LD_CONFIG_32

  [[ -f "\$LD_CONFIG_64" ]] && rm "\$LD_CONFIG_64"
  unset LD_CONFIG_64

  unset CONFIG_SHELL

  unset CC
  unset CFLAGS

  unset CXX
  unset CXXFLAGS


  unset LD
  unset LDFLAGS

  unset PKG_CONFIG_PATH
  export PATH=/usr/bin:/usr/sbin

  unset -f add-path
  unset -f add-pkgconfig-path
  unset -f extend-env
  unset -f cleanup-env
}

cleanup-env
 

EOF

touch "$MP_SOURCE/NOTES"

I would also suggest creating 3 additional datasets with somewhat similar structures at the same level as build and prototype. They would be:

  • source (gather all tarballs for every desired version)
  • package (gather final results or pre-built binaries tarballs)
  • test (scratch area with quite obvious purposes)

The whole staging area first children would be:

$ zfs list -r -d 1 -o name rpool/stage
NAME
rpool/stage

rpool/stage/build
rpool/stage/package
rpool/stage/prototype
rpool/stage/source
rpool/stage/test


$ l /stage
total 31
drwxrwxrwt   4 root  root     ... build
drwxrwxrwt  10 root  root     ... package
drwxrwxrwt   4 root  root     ... prototype
drwxrwxrwt   6 root  root     ... source
drwxrwxrwt   3 root  root     ... test