Replace the configure templating system with something better.

A large amount of this code can be removed when buildtool is done
as we will not need to worry about the differences between BSD and
GNU Make.
This commit is contained in:
Peter Powell 2014-04-14 12:42:22 +01:00 committed by Attila Molnar
parent b512f1b966
commit e6d245a3e8
10 changed files with 296 additions and 158 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
*.swp
/.config.cache
/.gdbargs
/.modulemanager
/BSDmakefile
/GNUmakefile

118
configure vendored
View File

@ -253,8 +253,7 @@ if ($interactive)
# Clear the screen.
system 'tput', 'clear';
my $revision = get_revision();
chomp(my $version = `sh src/version.sh`);
my %version = get_version();
# Display Introduction Message..
print <<"STOP" ;
@ -272,12 +271,12 @@ dir, otherwise you won't have a config file!
Your operating system is: \e[1;32m$^O\e[0m
STOP
print "Your InspIRCd version is: \e[1;32m";
print $revision eq 'release' ? substr($version, 9) : substr($revision, 1);
print "$version{MAJOR}.$version{MINOR}.$version{PATCH}+$version{LABEL}";
print "\e[0m\n\n";
print "The following compiler has been detected: \e[1;32m$cxx{NAME} $cxx{VERSION}\e[0m ($config{CXX})\n\n";
# Check that the user actually wants this version.
if (index($version, '+') != -1) {
if ($version{LABEL} ne 'release') {
print <<"EOW" ;
\e[1;31mWARNING!\e[0m You are building a development version. This contains code which has
not been tested as heavily and may contain various faults which could seriously
@ -417,7 +416,7 @@ if ($config{USE_GNUTLS} || $config{USE_OPENSSL}) {
print "Writing \e[1;32m.config.cache\e[0m ...\n";
write_configure_cache(%config);
writefiles();
parse_templates(\%config, \%cxx);
dump_hash();
print "\n";
@ -430,115 +429,6 @@ if ($config{USE_GNUTLS} || $config{USE_OPENSSL}) {
}
print "*** \e[1;32mRemember to edit your configuration files!!!\e[0m ***\n\n";
sub writefiles {
chomp(my $incos = `uname -n -s -r`);
chomp(my $version = `sh src/version.sh`);
my $revision = get_revision();
my $branch = "InspIRCd-0.0";
if ($version =~ /^(InspIRCd-[0-9]+\.[0-9]+)\.[0-9]+/)
{
$branch = $1;
}
print "Writing \e[1;32mconfig.h\e[0m\n";
open(FILEHANDLE, ">include/config.h.tmp");
print FILEHANDLE <<EOF;
/* Auto generated by configure, do not modify! */
#pragma once
#define BRANCH "$branch"
#define VERSION "$version"
#define REVISION "$revision"
#define SYSTEM "$incos"
#define INSPIRCD_SOCKETENGINE_NAME "$config{SOCKETENGINE}"
#define CONFIG_PATH "$config{CONFIG_DIR}"
#define DATA_PATH "$config{DATA_DIR}"
#define LOG_PATH "$config{LOG_DIR}"
#define MOD_PATH "$config{MODULE_DIR}"
EOF
if ($config{HAS_EVENTFD}) {
print FILEHANDLE "#define HAS_EVENTFD\n";
}
if ($config{HAS_CLOCK_GETTIME}) {
print FILEHANDLE "#define HAS_CLOCK_GETTIME\n";
}
print FILEHANDLE "\n#include \"threadengines/threadengine_pthread.h\"\n";
close(FILEHANDLE);
unlink 'include/config.h';
rename 'include/config.h.tmp', 'include/config.h';
# Do this once here, and cache it in the .*.inc files,
# rather than attempting to read src/version.sh from
# compiled code -- we might not have the source to hand.
my @dotfiles = qw(main.mk inspircd);
push @dotfiles, 'org.inspircd.plist' if $^O eq 'darwin';
foreach my $file (@dotfiles) {
open(FILEHANDLE, "make/template/$file") or die "Can't open make/template/$file: $!";
$_ = join '', <FILEHANDLE>;
close(FILEHANDLE);
$config{COMPILER} = lc $cxx{NAME};
$config{SYSTEM} = lc $^O;
for my $var (qw(
CXX COMPILER SYSTEM BASE_DIR CONFIG_DIR MODULE_DIR BINARY_DIR DATA_DIR UID SOCKETENGINE
)) {
s/\@$var\@/$config{$var}/g;
}
s/\@VERSION\@/$version/ if defined $version;
if ($file eq 'main.mk') {
print "Writing \e[1;32mGNUmakefile\e[0m ...\n";
my $mk_tmp = $_;
s/\@IFDEF (\S+)/ifdef $1/g;
s/\@IFNDEF (\S+)/ifndef $1/g;
s/\@IFEQ (\S+) (\S+)/ifeq ($1,$2)/g;
s/\@IFNEQ (\S+) (\S+)/ifneq ($1,$2)/g;
s/\@ELSIFEQ (\S+) (\S+)/else ifeq ($1,$2)/g;
s/\@ELSE/else/g;
s/\@ENDIF/endif/g;
s/ *\@BSD_ONLY .*\n//g;
s/\@GNU_ONLY //g;
s/\@DO_EXPORT (.*)/export $1/g;
open MKF, '>GNUmakefile' or die "Can't write to GNUmakefile: $!";
print MKF $_;
close MKF;
print "Writing \e[1;32mBSDmakefile\e[0m ...\n";
$_ = $mk_tmp;
s/\@IFDEF (\S+)/.if defined($1)/g;
s/\@IFNDEF (\S+)/.if !defined($1)/g;
s/\@IFEQ (\S+) (\S+)/.if $1 == $2/g;
s/\@IFNEQ (\S+) (\S+)/.if $1 != $2/g;
s/\@ELSIFEQ (\S+) (\S+)/.elif $1 == $2/g;
s/\@ELSE/.else/g;
s/\@ENDIF/.endif/g;
s/\@BSD_ONLY //g;
s/ *\@GNU_ONLY .*\n//g;
$mk_tmp = $_;
$mk_tmp =~ s#\@DO_EXPORT (.*)#"MAKEENV += ".join ' ', map "$_='\${$_}'", split /\s/, $1#eg;
open MKF, '>BSDmakefile' or die "Can't write to BSDmakefile: $!";
print MKF $mk_tmp;
close MKF;
} else {
print "Writing \e[1;32m$file\e[0m ...\n";
open(FILEHANDLE, ">$file") or die("Can't write to $file: $!\n");
print FILEHANDLE $_;
close(FILEHANDLE);
}
}
chmod 0750, 'inspircd';
}
# Routine to list out the extra/ modules that have been enabled.
# Note: when getting any filenames out and comparing, it's important to lc it if the
# file system is not case-sensitive (== Epoc, MacOS, OS/2 (incl DOS/DJGPP), VMS, Win32

View File

@ -106,9 +106,11 @@
*/
#if defined _WIN32
# include "inspircd_win32wrapper.h"
# include "threadengines/threadengine_win32.h"
#else
# include <unistd.h>
# define ENTRYPOINT int main(int argc, char** argv)
# define DllExport __attribute__ ((visibility ("default")))
# define CoreExport __attribute__ ((visibility ("default")))
# include <unistd.h>
# include "threadengines/threadengine_pthread.h"
#endif

View File

@ -1,7 +1,7 @@
#
# InspIRCd -- Internet Relay Chat Daemon
#
# Copyright (C) 2012 Peter Powell <petpow@saberuk.com>
# Copyright (C) 2012-2014 Peter Powell <petpow@saberuk.com>
# Copyright (C) 2008 Robin Burchell <robin+git@viroteck.net>
# Copyright (C) 2007-2008 Craig Edwards <craigedwards@brainbox.cc>
# Copyright (C) 2008 Thomas Stagner <aquanight@inspircd.org>
@ -30,8 +30,9 @@ package make::configure;
use strict;
use warnings FATAL => qw(all);
use Cwd;
use Cwd 'getcwd';
use Exporter 'import';
use File::Basename 'basename';
use make::utilities;
@ -39,10 +40,7 @@ our @EXPORT = qw(cmd_clean cmd_help cmd_update
read_configure_cache write_configure_cache
get_compiler_info find_compiler
run_test test_file test_header
get_property get_revision
dump_hash);
my $revision;
dump_hash get_property parse_templates);
sub __get_socketengines() {
my @socketengines;
@ -53,6 +51,34 @@ sub __get_socketengines() {
return @socketengines;
}
# TODO: when buildtool is done this can be mostly removed with
# the remainder being merged into parse_templates.
sub __get_template_settings($$) {
# These are actually hash references
my ($config, $compiler) = @_;
# Start off by populating with the config
my %settings = %$config;
# Compiler information
while (my ($key, $value) = each %{$compiler}) {
$settings{'COMPILER_' . $key} = $value;
}
# Version information
my %version = get_version();
while (my ($key, $value) = each %version) {
$settings{'VERSION_' . $key} = $value;
}
# Miscellaneous information
$settings{SYSTEM_NAME} = lc $^O;
chomp($settings{SYSTEM_NAME_VERSION} = `uname -sr 2>/dev/null`);
return %settings;
}
sub cmd_clean {
unlink '.config.cache';
}
@ -129,9 +155,9 @@ sub cmd_update {
exit 1;
}
print "Updating...\n";
%main::config = read_configure_cache();
%main::cxx = get_compiler_info($main::config{CXX});
main::writefiles();
my %config = read_configure_cache();
my %compiler = get_compiler_info($config{CXX});
parse_templates(\%config, \%compiler);
print "Update complete!\n";
exit 0;
}
@ -241,15 +267,7 @@ sub get_property($$;$)
return defined $default ? $default : '';
}
sub get_revision {
return $revision if defined $revision;
chomp(my $tags = `git describe --tags 2>/dev/null`);
$revision = $tags || 'release';
return $revision;
}
sub dump_hash()
{
sub dump_hash() {
print "\n\e[1;32mPre-build configuration is complete!\e[0m\n\n";
print "\e[0mBase install path:\e[1;32m\t\t$main::config{BASE_DIR}\e[0m\n";
print "\e[0mConfig path:\e[1;32m\t\t\t$main::config{CONFIG_DIR}\e[0m\n";
@ -262,4 +280,163 @@ sub dump_hash()
print "\e[0mOpenSSL support:\e[1;32m\t\t$main::config{USE_OPENSSL}\e[0m\n";
}
sub parse_templates($$) {
# These are actually hash references
my ($config, $compiler) = @_;
# Collect settings to be used when generating files
my %settings = __get_template_settings($config, $compiler);
# Iterate through files in make/template.
foreach (<make/template/*>) {
print "Parsing $_...\n";
open(TEMPLATE, $_);
my (@lines, $mode, @platforms, %targets);
# First pass: parse template variables and directives.
while (my $line = <TEMPLATE>) {
chomp $line;
# Does this line match a variable?
while ($line =~ /(@(\w+?)@)/) {
my ($variable, $name) = ($1, $2);
if (defined $settings{$name}) {
$line =~ s/$variable/$settings{$name}/;
} else {
print STDERR "Warning: unknown template variable '$name' in $_!\n";
last;
}
}
# Does this line match a directive?
if ($line =~ /^\s*%(\w+)\s+(.+)$/) {
if ($1 eq 'define') {
if ($settings{$2}) {
push @lines, "#define $2";
} else {
push @lines, "#undef $2";
}
} elsif ($1 eq 'mode') {
$mode = oct $2;
} elsif ($1 eq 'platform') {
push @platforms, $2;
} elsif ($1 eq 'target') {
if ($2 =~ /(\w+)\s(.+)/) {
$targets{$1} = $2;
} else {
$targets{DEFAULT} = $2;
}
} else {
print STDERR "Warning: unknown template command '$1' in $_!\n";
push @lines, $line;
}
next;
}
push @lines, $line;
}
close(TEMPLATE);
# Only proceed if this file should be templated on this platform.
if ($#platforms < 0 || grep { $_ eq $^O } @platforms) {
# Add a default target if the template has not defined one.
unless (scalar keys %targets) {
$targets{DEFAULT} = basename $_;
}
# Second pass: parse makefile junk and write files.
while (my ($name, $target) = each %targets) {
# TODO: when buildtool is done this mess can be removed completely.
my @final_lines;
foreach my $line (@lines) {
# Are we parsing a makefile and does this line match a statement?
if ($name =~ /(?:BSD|GNU)_MAKE/ && $line =~ /^\s*\@(\w+)(?:\s+(.+))?$/) {
my @tokens = split /\s/, $2 if defined $2;
if ($1 eq 'DO_EXPORT' && defined $2) {
if ($name eq 'BSD_MAKE') {
foreach my $variable (@tokens) {
push @final_lines, "MAKEENV += $variable='\${$variable}'";
}
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "export $2";
}
} elsif ($1 eq 'ELSE') {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".else";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "else";
}
} elsif ($1 eq 'ENDIF') {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".endif";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "endif";
}
} elsif ($1 eq 'ELSIFEQ' && defined $2) {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".elif $tokens[0] == $tokens[1]";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "else ifeq ($tokens[0], $tokens[1])";
}
} elsif ($1 eq 'IFDEF' && defined $2) {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".if defined($2)";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "ifdef $2";
}
} elsif ($1 eq 'IFEQ' && defined $2) {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".if $tokens[0] == $tokens[1]";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "ifeq ($tokens[0],$tokens[1])";
}
} elsif ($1 eq 'IFNEQ' && defined $2) {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".if $tokens[0] != $tokens[1]";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "ifneq ($tokens[0],$tokens[1])";
}
} elsif ($1 eq 'IFNDEF' && defined $2) {
if ($name eq 'BSD_MAKE') {
push @final_lines, ".if !defined($2)";
} elsif ($name eq 'GNU_MAKE') {
push @final_lines, "ifndef $2";
}
} elsif ($1 eq 'TARGET' && defined $2) {
if ($tokens[0] eq $name) {
push @final_lines, substr($2, length($tokens[0]) + 1);
}
} elsif ($1 !~ /[A-Z]/) {
# HACK: silently ignore if lower case as these are probably make commands.
push @final_lines, $line;
} else {
print STDERR "Warning: unknown template command '$1' in $_!\n";
push @final_lines, $line;
}
next;
}
push @final_lines, $line;
}
# Write the template file.
print "Writing $target...\n";
open(TARGET, ">$target");
foreach (@final_lines) {
print TARGET $_, "\n";
}
close(TARGET);
# Set file permissions.
if (defined $mode) {
chmod $mode, $target;
}
}
}
}
}
1;

38
make/template/config.h Normal file
View File

@ -0,0 +1,38 @@
/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2014 Peter Powell <petpow@saberuk.com>
*
* This file is part of InspIRCd. InspIRCd 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, version 2.
*
* 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 General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#define BRANCH "InspIRCd-@VERSION_MAJOR@.@VERSION_MINOR@"
#define VERSION "InspIRCd-@VERSION_MAJOR@.@VERSION_MINOR@.@VERSION_PATCH@"
#define REVISION "@VERSION_LABEL@"
#define SYSTEM "@SYSTEM_NAME_VERSION@"
#define CONFIG_PATH "@CONFIG_DIR@"
#define DATA_PATH "@DATA_DIR@"
#define LOG_PATH "@LOG_DIR@"
#define MOD_PATH "@MODULE_DIR@"
#define INSPIRCD_SOCKETENGINE_NAME "@SOCKETENGINE@"
#ifndef _WIN32
%target include/config.h
%define HAS_CLOCK_GETTIME
%define HAS_EVENTFD
#endif

View File

@ -1,3 +1,4 @@
%target .gdbargs
handle SIGPIPE pass nostop noprint
handle SIGHUP pass nostop noprint
run

View File

@ -1,3 +1,4 @@
%mode 0750
#!/usr/bin/env perl
#
@ -30,7 +31,7 @@ my $runpath = "@BASE_DIR@";
my $datadir = "@DATA_DIR@";
my $valgrindlogpath = "$basepath/valgrindlogs";
my $executable = "inspircd";
my $version = "@VERSION@";
my $version = "@VERSION_MAJOR@.@VERSION_MINOR@.@VERSION_PATCH@+@VERSION_LABEL@";
my $uid = "@UID@";
if (!("--runasroot" ~~ @ARGV) && ($< == 0 || $> == 0)) {

View File

@ -1,3 +1,5 @@
%target BSD_MAKE BSDmakefile
%target GNU_MAKE GNUmakefile
#
# InspIRCd -- Internet Relay Chat Daemon
#
@ -31,8 +33,8 @@
CXX = @CXX@
COMPILER = @COMPILER@
SYSTEM = @SYSTEM@
COMPILER = @COMPILER_NAME@
SYSTEM = @SYSTEM_NAME@
BUILDPATH ?= $(PWD)/build
SOCKETENGINE = @SOCKETENGINE@
CORECXXFLAGS = -fPIC -fvisibility-inlines-hidden -pipe -Iinclude -Wall -Wextra -Wfatal-errors -Wno-unused-parameter -Wshadow
@ -50,11 +52,11 @@ INSTMODE_DIR = 0750
INSTMODE_BIN = 0750
INSTMODE_LIB = 0640
@IFNEQ $(COMPILER) icc
@IFNEQ $(COMPILER) ICC
CORECXXFLAGS += -pedantic -Woverloaded-virtual -Wshadow -Wformat=2 -Wmissing-format-attribute
@ENDIF
@IFNEQ $(SYSTEM)-$(COMPILER) darwin-gcc
@IFNEQ $(SYSTEM)-$(COMPILER) darwin-GCC
CORECXXFLAGS += -fvisibility=hidden
@ENDIF
@ -88,7 +90,7 @@ INSTMODE_LIB = 0640
DBGOK=0
@IFEQ $(D) 0
CORECXXFLAGS += -O2
@IFEQ $(CXX) g++
@IFEQ $(COMPILER) GCC
CORECXXFLAGS += -g1
@ENDIF
HEADER = std-header
@ -106,18 +108,18 @@ DBGOK=0
@ENDIF
FOOTER = finishmessage
@GNU_ONLY MAKEFLAGS += --no-print-directory
@TARGET GNU_MAKE MAKEFLAGS += --no-print-directory
@GNU_ONLY SOURCEPATH = $(shell /bin/pwd)
@BSD_ONLY SOURCEPATH != /bin/pwd
@TARGET GNU_MAKE SOURCEPATH = $(shell /bin/pwd)
@TARGET BSD_MAKE SOURCEPATH != /bin/pwd
@IFDEF V
RUNCC = $(CXX)
RUNLD = $(CXX)
VERBOSE = -v
@ELSE
@GNU_ONLY MAKEFLAGS += --silent
@BSD_ONLY MAKE += -s
@TARGET GNU_MAKE MAKEFLAGS += --silent
@TARGET BSD_MAKE MAKE += -s
RUNCC = perl $(SOURCEPATH)/make/run-cc.pl $(CXX)
RUNLD = perl $(SOURCEPATH)/make/run-cc.pl $(CXX)
@ENDIF
@ -139,8 +141,8 @@ TARGET = all
@IFDEF M
HEADER = mod-header
FOOTER = mod-footer
@BSD_ONLY TARGET = modules/${M:S/.so$//}.so
@GNU_ONLY TARGET = modules/$(M:.so=).so
@TARGET BSD_MAKE TARGET = modules/${M:S/.so$//}.so
@TARGET GNU_MAKE TARGET = modules/$(M:.so=).so
@ENDIF
@IFDEF T
@ -229,11 +231,11 @@ install: target
[ $(BUILDPATH)/modules/ -ef $(MODPATH) ] || $(INSTALL) -m $(INSTMODE_LIB) $(BUILDPATH)/modules/*.so $(MODPATH)
@ENDIF
-$(INSTALL) -m $(INSTMODE_BIN) inspircd $(BASE) 2>/dev/null
-$(INSTALL) -m $(INSTMODE_LIB) .gdbargs $(BASE)/.gdbargs 2>/dev/null
@IFEQ $(SYSTEM) darwin
-$(INSTALL) -m $(INSTMODE_BIN) org.inspircd.plist $(BASE) 2>/dev/null
@ENDIF
-$(INSTALL) -m $(INSTMODE_BIN) tools/genssl $(BINPATH)/inspircd-genssl 2>/dev/null
-$(INSTALL) -m $(INSTMODE_LIB) tools/gdbargs $(BASE)/.gdbargs 2>/dev/null
-$(INSTALL) -m $(INSTMODE_LIB) docs/conf/*.example $(CONPATH)/examples
-$(INSTALL) -m $(INSTMODE_LIB) docs/conf/aliases/*.example $(CONPATH)/examples/aliases
-$(INSTALL) -m $(INSTMODE_LIB) docs/conf/modules/*.example $(CONPATH)/examples/modules
@ -253,7 +255,7 @@ install: target
GNUmakefile BSDmakefile: make/template/main.mk src/version.sh configure .config.cache
./configure -update
@BSD_ONLY .MAKEFILEDEPS: BSDmakefile
@TARGET BSD_MAKE .MAKEFILEDEPS: BSDmakefile
clean:
@echo Cleaning...

View File

@ -1,3 +1,4 @@
%platform darwin
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

View File

@ -36,19 +36,44 @@ use File::Spec::Functions qw(rel2abs);
use Getopt::Long;
use POSIX;
our @EXPORT = qw(module_installed prompt_bool prompt_dir prompt_string get_cpu_count make_rpath pkgconfig_get_include_dirs pkgconfig_get_lib_dirs pkgconfig_check_version translate_functions promptstring);
# Parse the output of a *_config program,
# such as pcre_config, take out the -L
# directive and return an rpath for it.
# \e[1;32msrc/Makefile\e[0m
our @EXPORT = qw(get_version module_installed prompt_bool prompt_dir prompt_string get_cpu_count make_rpath pkgconfig_get_include_dirs pkgconfig_get_lib_dirs pkgconfig_check_version translate_functions promptstring);
my %already_added = ();
my $if_skip_lines = 0;
my %version = ();
sub module_installed($)
{
sub get_version {
return %version if %version;
# Attempt to retrieve version information from src/version.sh
chomp(my $vf = `sh src/version.sh 2>/dev/null`);
if ($vf =~ /^InspIRCd-([0-9]+)\.([0-9]+)\.([0-9]+)(?:\+(\w+))?$/) {
%version = ( MAJOR => $1, MINOR => $2, PATCH => $3, LABEL => $4 );
}
# Attempt to retrieve missing version information from Git
chomp(my $gr = `git describe --tags 2>/dev/null`);
if ($gr =~ /^v([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\d+-(\w+))?$/) {
$version{MAJOR} = $1 unless defined $version{MAJOR};
$version{MINOR} = $2 unless defined $version{MINOR};
$version{PATCH} = $3 unless defined $version{PATCH};
$version{LABEL} = $4 if defined $4;
}
# The user is using a stable release which does not have
# a label attached.
$version{LABEL} = 'release' unless defined $version{LABEL};
# If any of these fields are missing then the user has deleted the
# version file and is not running from Git. Fill in the fields with
# dummy data so we don't get into trouble with undef values later.
$version{MAJOR} = '0' unless defined $version{MAJOR};
$version{MINOR} = '0' unless defined $version{MINOR};
$version{PATCH} = '0' unless defined $version{PATCH};
return %version;
}
sub module_installed($) {
my $module = shift;
eval("use $module;");
return !$@;