/*************************************************************************** * main.c * * Mon Apr 18 15:19:04 2005 * Copyright 2005 Benjamin Ducke ****************************************************************************/ /* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* TODO: for 1.0: EASY STUFF: - include $GISBASE/lib in linker path for compilation of extensions [NOT FIXED: see lines preceeding INSTALL action case: 693 -L is added to command line, but problems still exist] MAYBE EXPORT LD_LIBRARY_PATH ??? (only add $GISBASE/lib if it does not already exist) - finish skeleton files: include necessary make stuff for most popular architectures - make sure that no unnecessary stuff is installed in the top-level Makefile section for real-install - lots of files in the make system still output to /dev/null (cygwin) !!! - make install action to copy executable GEM to /usr/local or wherever grass6X script gets installed to - all files installed by GEM and the Makefile sections referring to GEM are chown'd root. Can that be a problem? for 1.2 (GRASS 6.2): - make GRASS store its ./configure command line options in a file in the GISBASE/etc directory, so that it will be possible for GEM to automatically configure extensions according to the system setup [this means that configure file needs to be kept in sync with GEM; --configure option can be used to overwrite this behaviour] - configure script should not fail but disable options and create config.msgs the contents of this should be displayed and deleted afterwards - check if it works with this Mac version of GRASS, as well: http://openosx.com/grass/ - before release: any problems expected with native Win32-Kompilation?: - location for tmp-files? - a simple wrapper g.install with GRASS style parameters. Should start an xterm and ask for su pwd, if necessary - action to check dependencies of installed extensions - instead of aborting on each failed dependency: build list of all failed dependencies before aborting - mechanism to recursively download and install missing deps from a provided list of URLs - instead of calling external tar, switch to tarlib and zlib - action --validate for checking an extension archive/dir - add link to HTML help index into each extension's submenu in GIS Manager - g.install with a real custom-made GUI [VARIABLE GUI must be set appropriately in run_post() for post script] this should be an independent Tcl/Tk GUI: install/uninstall/query extensions - extension database on the internet that can be queried and used with either gem or g.install - entries for QGIS - simplified scheme for installing just a single module - new scheme for registering menu entries in GIS Manager with stable hooks in menu.tcl NEED HELP - GRASS' make install installs all files BENEATH top level dir with UID set to benni !!! Is this intentional? - provide gem61 as link in /usr/local/bin as part of grass 6.1 base install - description.html should not contain or as Rules.Make seems to append those ? - source install copies files COPYING README REQUIREMENTS.html to somewhere (but where?) -> into /src but where else ? - menu.tcl in GRASS 6.0.0 has "all options 1", but 6.1 will have "all options $tmenu". How to cater for such things? Maybe in the future there should always be an Xtns menu provided by GIS Manager and always delimited by reliable tags - d.m window is a bit to small horizontally CAVEATS: - does not allow installation of different versions of the same extension within the same GRASS bin tree - does not have a real upgrading mechanism: option --upgrade has been disabled for now - restore cannot fix partially corrupted entries in HTML and TCL files - Links to other extension's modules will only work if those extensions are installed DOCS: - skeleton contains GPL as default license. Creators of new extensions need to be aware of this! - menu.tcl will be backed up as menu.tcl.gem.bak - HTML docs: users must prefix references to GRASS modules outside the Extension with "../../html/" !!! - description files: things inside "<" and ">" will be filtered out as HTML tags, even if they are none! - make clear that there are some files which will be rendered both as text and HTML and therefore need things like
while others DO NOT (which ones?)! - configure script should not fail but disable options and create 'config.msgs' - deletes config.msg after display - State clearly that it is recommended to uninstall an older extension version before installing a new one. STATE CLEARLY that --upgrade is rather another version of --force than a real updating mechanism (RENAME to --newer) - document all env vars that gem sets - STATE CLEARLY, that users should not meddle with stuff in index.html, menu.tcl, gem-entries/ and docs/extensions/ !!! - provide a unix man page, ASCII and HTML documentation (write HTML and convert) - files in the skeleton should always be present, even if they do not containing information - make it clear, that uninstall and postinstall are run with su privileges! thus, author must make sure, that only objects in the GRASS install tree are affected - user may specify either file name or extension name for uninstall action - user may list installed extensions by using -q w/o filename. Explain what is meant by type (e.g. 'src') - uninstallation works only by extension name, NOT file name ! - compiling extensions on a GRASS install with all but the most basic options disabled should give very portable binaries! (statically linked binaries?) BUGS: - remove cva, install again: WARNING: list item 'cva' exists in index.html (?) - superfluous warning upon uninstall of extension w/o "entries-gisman" - due to bad command line parsing, listing installed extensions only works like this: ./gem --grass=/usr/local/grass-6.1.cvs -q NOT like this: ./gem -q --grass=/usr/local/grass-6.1.cvs */ #define LOCAL #include "globals.h" #include #include void show_help ( void ) { fprintf (stdout, "Usage: gem [OPTION] [ACTION] [FILE|DIR]\n"); fprintf (stdout, "Install a GRASS extension from FILE or DIR.\n"); fprintf (stdout, "Manage (installed) GRASS extension(s).\n"); fprintf (stdout, "\nPossible ACTIONs are:\n"); fprintf (stdout, " -i, --install=\tinstall a GRASS extension\n"); fprintf (stdout, " -u, --uninstall=\tremove an extension from GRASS\n"); fprintf (stdout, " -q, --query=\t\tdisplay information about extension/list installed\n"); fprintf (stdout, " -d, --details=\tdisplay additional details about an extension\n"); fprintf (stdout, " -c, --clean=\t\tclean extension's source code directories\n"); fprintf (stdout, " -t, --test=\t\tconfigure and compile extension, but don't install\n"); fprintf (stdout, " -l, --license=\tshow copyright information for an extension\n"); fprintf (stdout, " -r, --restore\t\trecreate HTML links and GIS Manager entries\n"); fprintf (stdout, " -h, --help\t\tdisplay this help and exit\n"); fprintf (stdout, " -V, --version\t\toutput version information and exit\n\n"); fprintf (stdout, "\nPossible OPTIONs are:\n"); fprintf (stdout, " -g, --grass=PATH\tpath to GRASS installation dir\n"); fprintf (stdout, " -b, --binary=NAME\tno compilation: use binary files for system NAME\n"); fprintf (stdout, " -f, --force\t\tforce action, regardless of dependencies\n"); fprintf (stdout, " -v, --verbose\t\tdisplay detailed status information\n"); fprintf (stdout, " -x, --configure=OPTS\tpass OPTS to configure script\n"); fprintf (stdout, " -s, --skip-config\tskip configure script\n"); fprintf (stdout, " -o, --options\t\toptions to pass to the C compiler/linker\n"); fprintf (stdout, "\nWhen run from within a GRASS session, locations of libs, header files\n"); fprintf (stdout, "and installation target dir will be assumed to match those of the active\n"); fprintf (stdout, "GRASS version. "); fprintf (stdout, "Option -g can be used to override these or install extensions\nfrom outside"); fprintf (stdout, "of a GRASS session.\n"); fprintf (stdout, "Per default, extensions will be compiled from source and then installed.\n"); fprintf (stdout, "If the exension package contains binaries for the user's platform, they can\n"); fprintf (stdout, "be installed instead using the -b option. "); fprintf (stdout, "For installation from source code, a C compiler and make tools are needed.\n"); fprintf (stdout, "\nExample:\n"); fprintf (stdout, "\tgem -b macosx --grass=/usr/local/grass-6.2.0 -i myExtension\n"); fprintf (stdout, "Installs the MacOS X binaries for 'myExtension' in /usr/local/grass-6.2.0.\n"); exit (0); } void show_details ( char *package ) { int error; char tmp [MAXSTR]; sprintf (tmp, "%s", basename (package) ); error = chdir ( tmp ); if ( error < 0 ) { print_error ( ERR_NO_ACCESS_EXT, "extension '%s' not accessible: (%s)\n", package, strerror (errno)); } dump_ascii ("info", "Detailed information"); /* run post action script */ system ("sh post"); exit (0); } void show_license ( char *package ) { int error; char tmp [MAXSTR]; sprintf (tmp, "%s", basename (package) ); error = chdir ( tmp ); if ( error < 0 ) { print_error ( ERR_NO_ACCESS_EXT, "extension '%s' not accessible: (%s)\n", package, strerror (errno)); } dump_ascii ("license", "Detailed information"); /* run post action script */ system ("sh post"); exit (0); } void show_version ( void ) { fprintf (stdout, "gem (GRASS extensions manager) %.2f\n", PROGVERSION); fprintf (stdout, "Written by Benjamin Ducke\n"); fprintf (stdout, "\nCopyright (C) 2005 Benjamin Ducke\n"); fprintf (stdout, "This is free software; see the source for copying conditions. There is NO\n"); fprintf (stdout, "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); exit (0); } /* determine options to pass to extension's configure script */ /* TODO: check, if system configuration meets a set of requirements */ /* THIS FUNCTION IS CURRENTLY NOT USED */ void get_configure_options ( char *gisbase ) { FILE *fp; char str [MAXSTR]; if ( strcmp ( CONFIG_OPTS, "" ) ) { /* if user has specified config options on the GEM command line: override anything else */ return; } /* check if GISBASE/etc/config.system exists and if so, read options from it */ sprintf ( str, "%s/etc/config.system", gisbase ); fp = fopen ( str, "r" ); if ( fp == NULL ) { print_warning ("could not open %s for read access. Using default configure options.\n", str ); return; } /* config.system may also contain nothing, only comments and/or whitespace */ if ( nc_fgets_nb ( str, MAXSTR, fp ) != NULL ) { strcpy ( CONFIG_OPTS, str ); } } int main (int argc, char *argv[]) { char *gisbase; char *grass_version; char *grass_major; char *grass_minor; char *grass_revision; char *tmp; char *url; char *filepart; char version_file [MAXSTR]; char package [MAXSTR]; char orgname [MAXSTR]; char pkg_name [MAXSTR]; char *bins; int pkg_major, pkg_minor, pkg_revision; FILE *f; int fd; char pkg_short_name [MAXSTR]; char invocation [MAXSTR]; char coptions [MAXSTR]; int major, minor, revision; int option; int action; int valid; struct stat buf; struct stat buf2; int error; int is_directory = 0; DIR *dir; struct dirent *dir_entry; int dir_found; int option_index = 0; static struct option long_options[] = { { "install", 1, NULL, 'i' }, { "uninstall", 1, NULL, 'u' }, { "query", 2, NULL, 'q' }, { "details", 1, NULL, 'd' }, { "clean", 1, NULL, 'c' }, { "test", 1, NULL, 't' }, { "license", 1, NULL, 'l' }, { "restore", 0, NULL, 'r' }, { "help", 0, NULL, 'h' }, { "version", 0, NULL, 'V' }, { "grass", 1, NULL, 'g' }, { "options", 1, NULL, 'o' }, { "binary", 1, NULL, 'b' }, { "force", 0, NULL, 'f' }, { "verbose", 0, NULL, 'v' }, { "skip-config", 0, NULL, 's' }, { "configure", 1, NULL, 'x' }, { 0, 0, 0, 0 } }; /* set global variables to defaults */ VERBOSE = 0; TMPCLEAN = 0; TMPDBCLEAN = 0; FORCE = 0; UPGRADE = 0; ERROR = 0; WARNINGS = 0; SKIP_CFG = 0; strcpy (GISMAN_CMD, ""); strcpy (GISMAN2_CMD, ""); strcpy (QGIS_CMD, ""); strcpy (UNINSTALL_CMD, ""); strcpy (HTML_CMD, ""); strcpy (TMPDIR,""); strcpy (TMPDB,""); strcpy (TMP_GISMAN, ""); strcpy (TMP_DESCR,""); strcpy (TMP_INFO,""); strcpy (TMP_DEPS,""); strcpy (TMP_BUGS,""); strcpy (TMP_AUTHORS,""); strcpy (TMP_HTML,""); strcpy (TMP_NULL,""); strcpy (CONFIG_OPTS,""); getcwd ( CWD, MAXSTR ); /* reset terminal colors */ fprintf (stdout, "\033[0m"); tmp = malloc (sizeof(char) * MAXSTR); strcpy (invocation, argv[0]); /* all output should be unbuffered */ setvbuf (stdout, (char *) NULL, _IONBF, 0); /* if run with no arguments: show help */ if ( argc == 1 ) { /* show usage info and exit */ show_help (); exit (0); } atexit ( &exit_msg ); /* show a message after program termination */ valid = 0; bins = NULL; gisbase = NULL; opterr = 0; option = getopt_long ( argc, argv, ":i:u:q:d:c:t:l:o:x:rhVg:b:fvs", long_options, &option_index ); while ( option != -1 ) { if ( option == '?' ) { print_error (ERR_INVOCATION,"unknown option or action specified.\n"); } /* check for missing arguments */ if ( option == ':' ) { if ( (optopt == 'i') || (optopt == 'u') || (optopt == 'd') || (optopt == 'c') || (optopt == 't') || (optopt == 'l') || (optopt == 'r') ) { print_error (ERR_INVOCATION,"missing file or directory name.\n"); } if ( optopt == 'g' ) { print_error (ERR_INVOCATION,"missing path to GRASS 6.\n"); } if ( optopt == 'b' ) { print_error (ERR_INVOCATION,"missing name of binary architecture.\n"); } if ( optopt == 'b' ) { print_error (ERR_INVOCATION,"missing configure options.\n"); } if ( optopt == 'q' ) { /* '-q' w/o filename is list action */ action = LIST; valid ++; break; } } if ( (option == 'i') || (option == 'u') || (option == 'q') || (option == 'd') || (option == 'c') || (option == 't') || (option == 'l') || (option == 'r') || (option == 'h') || (option == 'V') ) { /* got a valid action specifier */ valid ++; /* set action accordingly */ switch ( option ) { case 'i' : if ( action != BIN_INSTALL ) { action = INSTALL; } break; case 'u' : action = UNINSTALL; break; case 'q' : action = QUERY; break; case 'd' : action = DETAILS; break; case 'c' : action = CLEAN; break; case 't' : action = TEST_INSTALL; break; case 'l' : action = LICENSE; break; case 'r' : action = RESTORE; break; case 'h' : action = HELP; break; case 'V' : action = VERSION; break; } if ( optarg != NULL ) { /* save package name as given on command line */ strcpy ( package, optarg ); /* orgname will always preserve the commandline option */ strcpy ( orgname, optarg ); } } /* set options */ if ( option == 'g' ) { gisbase = malloc ( sizeof (char) * ( strlen ( optarg ) + 1 ) ); strcpy ( gisbase, optarg ); } if ( option == 'b' ) { bins = malloc ( sizeof (char) * ( strlen ( optarg ) + 1 ) ); strcpy ( bins, optarg ); action = BIN_INSTALL; } if ( option == 'x' ) { /* configure script options */ strcpy ( &CONFIG_OPTS[0], optarg ); } if ( option == 'f' ) { FORCE = 1; } if ( option == 'v' ) { VERBOSE = 1; } if ( option == 's' ) { SKIP_CFG = 1; } if ( option == 'o' ) { /* GEM_C_OPTS gets passed to the C compiler via the GRASS/GEM Makefiles: /src/include/Make/Grass.make.in: CFLAGS = $(INC) $(COMPILE_FLAGS) $(USE_TERMIO) $(GEM_C_OPTS) */ strcat (coptions, optarg); } /* get next option from command line */ option = getopt_long ( argc, argv, ":i:u:q:d:c:t:l:o:x:rhVg:b:fvs", long_options, &option_index ); } if ( valid < 1 ) { print_error ( ERR_INVOCATION,"please specify a valid action.\n" ); } if ( valid > 1 ) { print_error ( ERR_INVOCATION,"please specify only one action.\n"); } /* export compiler options for use by Makefiles */ setenv ("GEM_C_OPTS",coptions,1); if ( valid > 0 ) { /* export all relevant env vars for the post script */ if ( gisbase == NULL ) { /* try to read from GRASS environment */ gisbase = getenv ("GISBASE"); } run_post ( action, bins, gisbase ); } /* these actions can be done without any extension checking */ if ( action == HELP ) { /* show usage info and exit */ show_help (); exit (0); } if ( action == VERSION ) { /* show version info and exit */ show_version (); exit (0); } if (!VERBOSE) { /* set temp file to pipe output to for silent operation */ strcpy (TMP_NULL,"/tmp/grass.extension.log.XXXXXX"); /* TMP_NULL is a global variable */ fd = mkstemp ( TMP_NULL ); if ( fd == -1 ) { print_error ( ERR_TMPFILE, "could not create temp file: %s", strerror (errno)); exit (ERR_TMPFILE); } } /* these actions need a valid GRASS path but no extensions */ if ( action == RESTORE ) { /* figure out path to GRASS installation */ /* GIS base not given? */ if ( gisbase == NULL ) { /* try to read from GRASS environment */ gisbase = getenv ("GISBASE"); if ( gisbase == NULL) { /* still NULL? Abort! */ print_error ( ERR_INVOCATION, "GISBASE environment variable not set and path to GRASS not given.\n"); } } if ( VERBOSE ) { fprintf (stdout, "Path to GRASS is %s.\n", gisbase); } restore ( gisbase ); exit (0); } if ( action == LIST ) { /* figure out path to GRASS installation */ /* GIS base not given? */ if ( gisbase == NULL ) { /* try to read from GRASS environment */ gisbase = getenv ("GISBASE"); if ( gisbase == NULL) { /* still NULL? Abort! */ print_error ( ERR_INVOCATION, "GISBASE environment variable not set and path to GRASS not given.\n"); } } if ( VERBOSE ) { fprintf (stdout, "Path to GRASS is %s.\n", gisbase); } list_extensions ( gisbase ); exit (0); } /* check if extension is stored in a remote URL */ if ( (strstr (package, "http://")) || (strstr (package, "ftp://")) ) { wget_extension ( package ); /* download into current dir using wget */ /* cut off the path specification */ url = malloc (sizeof(char) * MAXSTR); strcpy (url, package); filepart = strrchr ( url, '/' ); filepart ++; strcpy (package, filepart); free (url); } if ( VERBOSE ) { fprintf (stdout, "Extension location is '%s'.\n", package); } if ( action != UNINSTALL ) { error = stat ( package, &buf ); if ( error < 0 ) { print_error ( ERR_NO_ACCESS_EXT, "extension FILE or DIR '%s' invalid: %s\n", package, strerror (errno)); } if ( S_ISDIR ( buf.st_mode ) ) { is_directory = 1; if ( VERBOSE ) { fprintf (stdout, "Extension files stored in a directory.\n"); } } else { if ( VERBOSE ) { fprintf (stdout, "Extension files stored in a package file.\n"); } /* DECOMPRESS INTO TEMP DIR, CHANGE NAME OF package TO THAT DIR */ unpack_extension ( package ); /* find name of directory containing extension files */ /* very primitive: just picks the first directory */ dir = opendir (TMPDIR); dir_entry = readdir (dir); dir_found = 0; while ( dir_entry != NULL ) { if ( (strcmp (dir_entry->d_name,".")) && (strcmp (dir_entry->d_name,"..")) ) { /* check if it is a directory */ sprintf ( tmp, "%s/%s", TMPDIR, dir_entry->d_name); stat ( tmp, &buf2 ); if ( S_ISDIR ( buf2.st_mode ) ) { dir_found = 1; break; } } dir_entry = readdir (dir); } strcpy ( package, tmp ); if ( dir_found == 0 ) { print_error ( ERR_UNPACK_EXT, "no top-level directory found in extension package.\n"); } } } /* copy package name into this maliciously named variable */ /* (sorry about the mess ...) */ /* This name will be used for all registration actions and */ /* for creating files and directories that store extension */ /* information for uninstall and restore actions */ /* For uninstall, we take the last argument as package, NOT */ /* file name */ if ( action == UNINSTALL ) { strcpy (pkg_short_name, package); } else { get_package_name ( package, pkg_short_name ); } if ( VERBOSE ) { fprintf (stdout, "Extension will be installed from '%s'\n", package); } /* CHECK PACKAGE FOR VALIDITY */ if ( action != UNINSTALL ) { check_extension ( package, pkg_name, &pkg_major, &pkg_minor, &pkg_revision ); } /* these actions can be done without GRASS checking */ if ( action == QUERY ) { query_extension ( package, pkg_name, pkg_major, pkg_minor, pkg_revision, pkg_short_name, invocation, orgname ); exit (0); } if ( action == DETAILS ) { show_details ( package ); exit (0); } if ( action == LICENSE ) { show_license ( package ); exit (0); } if ( action == CLEAN ) { source_clean ( package ); exit (0); } /* The following checks need to be done for all other actions! */ /* figure out path to GRASS installation */ /* GIS base not given? */ if ( gisbase == NULL ) { /* try to read from GRASS environment */ gisbase = getenv ("GISBASE"); if ( gisbase == NULL) { /* still NULL? Abort! */ print_error ( ERR_INVOCATION, "GISBASE environment variable not set and path to GRASS not given.\n"); } } if ( VERBOSE ) { fprintf (stdout, "Path to GRASS is %s.\n", gisbase); } /* figure out GRASS version */ grass_version = getenv ("GRASS_VERSION"); if ( grass_version == NULL ) { /* GRASS version can be read from gisbase/etc/VERSIONNUMBER */ sprintf ( version_file, "%s/etc/VERSIONNUMBER", gisbase ); f = fopen ( version_file, "r" ); if ( f == NULL ) { /* still NULL? Abort! */ print_error (ERR_VERSION, "Could not read GRASS version. Did you specify the right path?\n"); } else { grass_version = malloc ( sizeof(char) * 16); error = fscanf (f, "%s", grass_version); fclose (f); if ( error < 1 ) { print_error (ERR_VERSION, "Could not read GRASS version. Did you specify the right path?\n"); } } } if ( grass_version != NULL ) { /* extract major and minor version numbers */ tmp = strdup ( grass_version ); grass_major = strtok ( tmp, "." ); grass_minor = strtok (NULL, "." ); grass_revision = strtok (NULL, "." ); major = strtol ( grass_major, NULL, 10 ); minor = strtol ( grass_minor, NULL, 10 ); revision = strtol ( grass_revision, NULL, 10 ); grass_version = malloc (sizeof (char) * MAXSTR); sprintf (grass_version, "%i.%i.%i", major, minor, revision); if ( VERBOSE ) { fprintf (stdout, "GRASS version is %s.\n", grass_version); } if ( major < 6 ) { print_error ( ERR_VERSION, "extensions only work with GRASS version 6 and above.\n"); } } /* for GDAL compatibility and for the sake of people using Lorenzo Moretti's GRASS for MacOS, we always export the following compiler options */ sprintf (coptions, "-L%s/lib -I/usr/local/grasslib/include/ ", gisbase); /* these actions can only be done after everything has been checked */ if ( action == INSTALL ) { source_install ( package, gisbase, pkg_short_name, pkg_major, pkg_minor, pkg_revision, grass_version ); exit (0); } if ( action == UNINSTALL ) { uninstall ( package, pkg_short_name, gisbase ); exit (0); } if ( action == TEST_INSTALL ) { test_install ( package, gisbase, pkg_short_name, pkg_major, pkg_minor, pkg_revision, grass_version ); exit (0); } if ( action == BIN_INSTALL ) { if ( binaries_exist ( package, bins )) { bin_install ( package, gisbase, bins, pkg_short_name, pkg_major, pkg_minor, pkg_revision, grass_version ); exit (0); } else { print_error ( ERR_MISSING_BINS,"no binaries for system '%s'\n", bins); } } exit (0); }