Skip to content

Description

Sektor van Skijlen edited this page Apr 25, 2017 · 1 revision

Description

General idea

The general idea is very simple:

  1. You define a target of one of specified types. Typically it's a program (executable binary file) or a library.
  2. You should declare the minimum required things to build your target - first of all the source files.
  3. You may also declare some other data in a short-hand form - for example, a package name to be resolved with pkg-config command. In a worse case, you can declare the dependent libraries or header file path directly (although this may be non-portable).
  4. Now, Silvercat should generate the usual "make" build rules with appropriate commands and command-line options, together with additional things like clearing, installing etc.

The tool for managing the build rules is typically agmake, which is also part of this project and is described elsewhere.

For example:

ag prog1 -type program -sources src1.cpp src2.cpp -packages zlib

The standard processing facilities will then understand this declaration that:

  • this will be a program named "prog1" (possibly with some platform-dependent extension)
  • the source files are src1.cpp and src2.cpp
  • the common programming language is C++ (by .cpp extension on all files it was so recognized) and it should select C++ compiler and linker (you can also have sources in C and C++ in one definition and it will also select C++ for linker, as common one)
  • no installation rules are defined (the default -install is noinst and typical category for program is bin, which means installing in some directory typical for binary files - it's PREFIX/bin in POSIX systems)
  • there are no direct extra cflags and ldflags
  • basing on the -packages value (with default settings pkg-config framework is enabled) appropriate values will be added to -cflags and -ldflags, as resolved by pkg-config command

In general, it can be said that this undergoes two phases of processing:

  • translation phase: basing on values in some database keys, other keys get updated
  • generation phase: the makefile is generated basing on values found under appropriate keys (usually -rules), which is expected to have appropriate value for generation

Note that this is all about extracting the "interesting" data from the database. Therefore there's no "invalid option", at worst it can be an ignored option. It does matter though in what order the processing happens and that's why there are some builtin facilities and also hooks predicted to be filled in by frameworks.

Please note that some of the processing facilities, including frameworks, may also delete some data from the database. This happens, for example, with pkg-config framework, where the items from -packages key are being deleted once they are processed and turned into extra cflags and ldflags. This is to mark that the package has been already processed.

Finally it's expected that after all processing things are finished, we have:

  • defined rules for dependencies between target files and ingredients
  • composed command lines to execute to build particular target
  • defined other, specific targets, for example for cleaning after build, installation, packaging etc.

These all things should be then formed into explicit rule-command sets, using which next the Makefile.tcl is generated (it is at least theoretically possible to generate rules for some other rule-resolution language).

See more elaborate example at the end of this page.

Databases

There are generally the following databases:

  • profile: this keeps settings for all targets defined in the same Silvercat file (it's not spread to the underlying directories - for common settings use "include files", that is, separate files being source-d by every other Silvercat file). Data in the profiles are first keyed by programming language, although there happen to be also data that are language independent. Typically profiles contain definitions of the compiler command line for particular language, however they can also contain some typical target-related settings that should be in force for every target
  • target: this keeps settings for the given target. Note that some keys in the target may be propagated from profile
  • fileinfo: this keeps settings for a single file, residing anywhere. Some of the interesting data can be useful for further database processing. For example, you can have an "-includes" specification that defines what other header files belonging to the project are being included by a C/C++ file.
  • config: this is predicted to keep the general data that appear during various processing, but they are considered global

The most important facts about the databases are:

  • There's no such thing as "unknown" or "disallowed" key. A particular processing procedure (internal, framework etc.) may use data keyed to particular key, as long as it needs so, but whether there are any other keys - it doesn't matter. No framework needs to be aware of that there already is another framework in the system and whether it uses data keyed under the same keys or others.
  • Any processing procedure may potentially modify the database anyhow.

The profile is manipulated by adding profiles using the ag-profile command. This command takes the name of the profile prototype and applies its statements into the profile database. Every profile prototype can potentially contain data for various things, although usually for one domain. This means that multiple profiles can be applied one after another, there's nothing wrong with it, as long as you know which domain they concern. For example, applying multiple C++ compiler specific profile prototypes doesn't make sense because the second one will overwrite settings from the first one, making the first one ineffective. However, you can combine the compiler-specific profile and installation environment profile - they don't interfere with each other.

Basic syntax of Silverball statements

The file that defines the build statements for the current directory is typically named Makefile.ag or Silverball (possibly with .tcl suffix). Its role is similar to Makefile for make and Makefile.am for autotools. It's actually a Tcl script that is executed in an environment with available additional commands provided by Silvercat and Tclmake. Most of the statements in this script are declarations to modify the database.

The most important commands that modify particular databases are:

  • ag <target> [options...]: this command modifies the database of the given target
  • ag-info <file> [options...]: this command modifies the fileinfo database of the given file
  • ag-profile: runs in two flavors depending on the number of arguments:
    • <profile-name>: apply profile prototype of given name to the profile
    • <language> [options...]: modify the profile for the given language (or use "general" as the language to apply to all)

So the below syntax description concerns 'ag', 'ag-info', and 'ag-profile' commands (the role of <target> in ag command is a <filename> in ag-info command and <language> in ag-profile command).

The general syntax is:

ag <target> [-keyspec valuespec]...  # to update the value at 'keyspec'
ag <target> ?keyspec [mask]          # to query for a value

If the first character of the keyspec is "?", then it means a query. Such called 'ag' command (in Tcl sense) will then return the current value at given key. Optionally you can use a mask to filter out required values.

Normally you'd like to modify the database, so the key starts from "-" and multiple values are allowed. This means to update the value, which by default means that the given value is added to whatever is so far under that key.

NOTE: unlike you'd expect that for Tcl language, which is very strict about lists and expansion, Silvercat can interpret the argument for ag command in multiple ways, that is:

  1. You can enclose all arguments next to <target> in braces, which forms a single argument. This is comfortable as you are not obliged to observe the line breaks inside the braces!
  2. No matter the enclosing in braces (which means non-expanded text in Tcl), variable references are still resolved (if single argument is detected, the handler does extra variable expansion on its own).
  3. Every value in the -keyspec may be a list with elements. You can pass a complete list in braces (as normally in Tcl), or just pass subsequent elements of the list as subsequent arguments for the ag command.

For example:

ag dbl -sources db1.cc dbl.cc dblx.cc -headers h1.h db.h

or

ag dbl -sources {db1.cc dbl.cc dblx.cc} -headers {h1.h db.h}

or

ag dbl {
       -sources
           db1.cc
           dbl.cc
           dblx.cc
        -headers
            h1.h
            db.h
}

(DISCLAIMER: This is only a syntax example. -headers key is required only to declare installable headers for a library's devel package).

The value specification uses also additional special syntax trick (that's nothing to do with Tcl, it's Silvercat's private rule). The valuespec as a pure form without anything else means to (typically) update value, that is, append the given value to the existing one. Typically because the keys can have types, where the "default" type is "list". Several preselected keys have types:

  • single: Value update for that key will always make it overwritten
  • command: The value is treated as a single (or multiple) command line. Updating will append the text, but EOLs will be preserved.

There are four other special syntax cases then:

ag dbl -sources {- dbl.cc}  # remove "dbl.cc", db1.cc dblx.cc remain.
ag dbl -sources {= db1.cc dbl.cc} # overwrite current -source
ag dbl -sources {+ db3.cc}  # same as: -sources db3.cc
ag dbl -sources {++ db3.cc} # add db3.cc in front of the list

Note that for single-type keys removing or adding list elements is not allowed. Among special syntax cases only = is allowed.

This syntax is recognized by having a single argument, where the special symbol (-, =, +,++) is followed by a space. That's why for this type of syntax the "multiple arguments for an option" that are collected into a single list are not allowed here. The syntax with "+ " in the beginning seems to be redundant, but it may help in some corner cases. One of them is the case when the value itself starts from a dash, for example:

ag dbl -ldflags -lm

This actually does nothing but creating an "-lm" key without a value, where -ldflags is unchanged. The "-lm" isn't recognized here as an argument for -ldflags option. Doing {-lm} or "-lm" will obviously not help either. Here you can, however, use either of these solutions:

ag dbl -ldflags {+ -lm}  ;# the argument doesn't start from dash

or

ag dbl -ldflags -- -lm  ;# -- prevents interpreting -options til the end

The last one has one small disadvantage - you cannot pass any more "-keys" in this command. This isn't a big problem because the basic syntax means adding an element to the list, so it's quite normal to do:

ag dbl -sources db1.cc
ag dbl -ldflags -- -lz
if { $use_extras } {
    ag dbl -sources db_extras.cc
}

The syntax {++ element} seems to be insignificant for source files, as given in the example, however may be useful for adding libraries. For static libraries it unfortunately does matter in what order the libraries occur in the command line - the symbol requester must go first and the symbol provider must follow (unused symbols from the currently processed library are dropped instead of being remembered for prospective future use and only dropped in the end). Silvercat might be theoretically able to reorder libraries, but this requires several extra tools that analyze the symbols.

Example processing: program

This is one of the standard processing procedures.

The program is an executable binary file. So, it is stated that target of that type is something that must be defined in several source files, written in some programming language, for which we have a compiler. In the platforms that are in general use, each one should be compiled into an intermediate object file, then all these should be linked together, possibly with the addition of some libraries. For compiling, there may be also some extra options used, similarly for linking. There's also one extra target to be defined: installation procedure. There are also some specific data in the database that are meaningful for installation.

So, what "program" processor does is:

  1. Reads the "source" data. Each element is a source file.
  2. The "language" is recognized from the file's extension. For example, you have C language for .c extension and C++ language for .cc or .cpp extension. Of course, if this detection process doesn't work properly (or not at all), you can enforce language using -language key, in which case this value is taken as a good deal and no language detection is being done.
  3. Basing on the set profile, the compiler command line with possibly specific options is extracted for particular language. For example it's "gcc -c" for C language or "g++ -c" for C++ language. It depends on a language, which possibly intermediate files will be defined, together with their rules and commands to build.
  4. The command line is completed out of: the compiler command, the include directories, the macrodefinitions, and some other general "cflags" (note that "cflags" stands for "compile flags", not "C language flags") - this option may be used by both C and C++, unless the language doesn't use single-compile step.
  5. For every file matching the schema "NAME.LANGEXT", the new target (making rule) is defined as "NAME.ag.o" and the source for it is defined as the currently processed source file.

So, if we have a source file named "longtime.cc", we have identified C++ language and from the profile it has assigned compiler: "g++ -c". So finally we have a generated dependency: longtime.ag.o from longtime.cc and the command line:

gcc -c longtime.cc -o longtime.ag.o

And that's generally all. Next things are being performed by the "generator". The generation takes all these above processed data and does whatever is needed - to produce the target, at least in perspective.

In the current version, the "genrules" subcommand generates the Makefile for make.tcl (Makefile.tcl). The other command, "make", executes the rules in place.

Beside the rules that directly refer to the defined targets, there are two more things important for this generation:

  • installation and cleaning
  • frameworks

Installation and cleaning

Installation is simply to copy the built file into the installation directory. Mappings between category names and physical directories (as well as prefix that defaults to /usr/local on POSIX systems) are defined in the profile and may vary for various platforms. One of the most important category is "bin", which defines that the program is going to be installed in a standard binary directory. For POSIX systems, for example, it will be PREFIX/bin, where PREFIX defaults to /usr/local. If the category is "noinst" (default), the installation rules for this program will not be generated at all.

Cleaning in case of make.tcl is very simple because this tool has a builtin support for cleaning. It's enough to use "!tcl autoclean <target>" as a goal execution rule and this will walk through the rules to find the files for which the generation rules exist, starting from the given one. Files to be deleted are all those files, which are to be produced by the make process. Files, for which there are no building rules, are left untouched.

Profiles

A Profile contains various global data, which are predicted to control how Silvercat works, and partially also how the generated Makefile will work. The first key for this database is the language, although as many settings are language independent, the "general" key should be used in such cases.

Subkeys then contain things that influence various activities that may be done language-dependent, compiler-dependent or platform-dependent way. The following keys are used when generating compile commands in the rules:

  • compile: compile the single file into the form of the object file. Although this is predicted to be language independent, it should create a system-dependent file, generally the way as the assembler would do. This is unsuitable for languages, for which the compiler should compile all sources in one command line (e.g. Java)
  • compile_oflag: It's always "-o" by default, the option to define the name of the output file from compile command
  • link: a command to collect all object files (or source files, if object files haven't been declared for particular language) and create the required library or executable program
  • link_oflag: like above, the option for the compiler that should declare the name of the output file
  • linkdl: command to produce dynamic library, should link create a static library. If not used, defaults to link.
  • gendep: a command that should extract whatever files (existing in the project) in given source file are being "included" (should a language use such mechanism). If in particular environment there's no tool to do that, you should manually set this information for particular file using ag-info command.
  • preproc: a command to run preprocessing, should a language use a preprocessor
  • cflags: default flags passed to every compile command
  • ldflags: default flags passed to every link command

There are also some language-independent things, like installation definition:

  • installdir:* keys: directory for installing files of particular category:
    • :prefix: prefix for installation directory where the compiled things should be installed after being built
    • :bin: executable programs (POSIX: /usr/bin)
    • :lib: libraries (POSIX: /usr/lib)
    • :dll: dynamic libraries - optional, defaults to :lib (POSIX: /usr/lib, Cygwin: /usr/bin!)
    • :data: program-specific data of various kind (POSIX: /usr/share)
    • :include: header files

Config

This database is considered global, like Profile, however the main difference is that it doesn't control how Silvercat works, but rather how the specifics of sources and the platform should be treated, provided that the platform may contain various variants of the software, and the user has requested special options in the command line and turned on or off particular features, or requested using specific slotted version of some specific dependent software.

There's no specific description as to what there is and how to access it. Usually many of them turn into a set of definitions that are next instantiated into a header file with detailed settings - predicted to be next used in the code that should be controlled this way.

Frameworks

Frameworks are simply processing procedures that are being run at particular moments of the running ag script and possibly apply some modifications in the database.

As you know, the general idea of Silvercat is that the target, depending on its type, is being produced out of ingredients that have been earlier prepared. It completely doesn't matter if the required data have been provided explicitly (by defining them in the Makefile.ag.tcl file), or added by any of the processing parts - either standard, or coming from the framework.

Each part of this process is interested with particular data to be present in the database. For example, the program processor expects sources (in order to generate rules for compiling all sources and linking them together) plus some optional data (cflags, ldflags), as needed. You can add any data to the database as you like, in worst case they just won't be used (ok, in a really really bad case you'll get invalid data - so take care! :D).

This means that you can define whatever other data you need in the database - some of them will be probably only processed by a framework and during processing will be turned into additional data in the database, which then can be meaningful for other parts of the processing.

For example: a target can define the following:

ag dialog -s dlg1.cpp main.cpp -fw qt::app -packages gui

This is also an example of how data can be processed by pipeline in a specific order:

  • The qt::app framework will use its own facilities to recognize whether any of the header files need "moc" generation (it can also use internal abilities, the "gendep" entry from the language's profile, to extract dependent header files so that they can be tested for "mocability")
  • For any "mocable" headers, appropriate *.ag.moc.cpp file should be generated. This should be added as a source file to the database, with dependency on that header file (the standard processing for program type should do the rest).
  • The "-packages" option declares additional qt modules to be used. This can be implemented whatever good way is found, possibly with the internal use of pkg-config framework (one framework can call another framework on its own responsibility). The pkg-config framework is turned off if any framework is specified, so it will not try to resolve the package (unless added as second one).

Frameworks are generally to define what kind of processing should be done, especially if this processing has to end up with generating a C or C++ file to be next compiled and linked with the others. For example, if you want to have some protobuf things in your project, you have to add two things:

  1. A profile, which defines the compile process for *.proto files and language recognition.
  2. A framework, which will define a new rule to produce a *.pb.cpp and *,pb.h files from *.proto file, and add them to the -headers and -sources data in the database.

Database keys

There are some standard database keys used in internal processing. Some of the keys are only for target, although there are also keys that can be defined for the profile and they are later propagated to all targets defined in the same file. For example:

ag-profile general -defines _DEBUG

ag target1 -s source1.cc
ag target2 -s source2.cc

In this definition, the compile command line for both source1.cc and source2.cc will contain -D_DEBUG. Propagated keys are only some standard ones, however a framework can also propagate other flags on its own.

As profile-defined flags are defined per language, propagation depends on the languages - if a flag exists only for particular language, it will be propagated to the target only if the target's language matches the given language (the language for target is usually autodetected, although it can be still specified explicitly). The special value "general" means that it touches upon all languages. For example:

ag-profile c++ -cflags {+ -std=c++11} -ldflags {+ -lextracpp}
ag-profile c -cflags -- -std=c11

ag tar1 -s source1.c source2.cc -defines DEBUG

In this case the generated command lines will contain:

  • for source1.c there will be only -std=c11 flag (because its language is c) and -DDEBUG (as defined for tar1)
  • for source2.cc there will be only -std=c++11 flag (because its language is c++) and -DDEBUG (as defined for tar1)
  • for linking tar1 the -lextracpp flag will be added (because the common language for c and c++ is defined as c++, and this is the language set for the target). Note that -std... and -DDEBUG are -cflags, so they are not added for the link command.

The following are known typical keys used in various databases:

-type

  • (target only)

Defines the target type, which is the main information that defines how the target is going to be generally processed. Note that the processing happens only after all declaration-related instructions have been already executed. If the type is not defined, there are two possible default values: custom is used when there's a -command option also found, data if there's no -command, but still there is -sources option, and phony otherwise. See below for the more detailed information about possible target types.

-sources (alias -s)

  • (target only)

The source files. Used by targets of type library, program and data, generally meaning the files that are being read when the target is being built (also "input" files of various kinds).

-headers (alias -h)

  • (target only)

The installable header files. Used by target of type library with library specification devel to define header files to be installed for that library's devel package

-noinst-headers (alias -nh)

  • (target only)
  • (internally generated)

The non-installable header files. Currently none of the standard facility use them, however this is a kind-of standard key to interchange information about the header files used for compiling only. For example, all header files that have been extracted as ingredients of a C/C++ file (using dependency generator) from files defined in -sources for particular target, are automatically added to -noinst-headers, unless they are already found in -headers. This is to make them available to other processing facilities, for example frameworks

-cflags

  • (propagated from profile)

Compile flags, as mostly used for compiling C/C++ source files. Please note that languages like D or Java don't use compile-link steps, so they don't use this flag as well

-ldflags (alias -lflags, -libs)

  • (propagated from profile)

Link flags, used when linking all object and library files. Note that for languages that do not use intermediate object files (Java, D), this is the only place to put any command-line flags.

-defines (alias -D)

  • (propagated from profile)

Macro definitions to be defined for command line, lately translated into -cflags using compiler-specific macro definition command line option. Only for targets of type program or library.

-incdir (alias -I, that is uppercase "i")

  • (propagated from profile)

This specifies the include directories. Only for targets of type program or library

-libdir (alias -L)

  • (propagated from profile)

This specifies the library directories. Only for targets of type program or library (effective only for dynamic libraries)

-libspec

  • (target only)
  • (specific for type library)

Used to specify what flavor should be used for libraries. Meaningful only when -type is library (see below)

-install

  • (target only)

Specifies installation category (see below). Alternative specification for installation category is INSTALL:TYPE specification for -type key (e.g. -type bin:program is equivalent to -type program -install bin). If this option isn't specified, it defaults to noinst, which means that the built target is not to be installed (install goals in the rules will not be generated).

-packages

  • (propagated from profile)

Defines package names to be used by target. By default it's handled by pkg-config framework, which is turned on by default (however another framework may change this behavior). The pkg-config framework makes these names lately translated to appropriate -cflags and -ldflags, as extracted from pkg-config command line (all such packages are removed from the list so that packages unhandled by pkg-config can be retried with some other framework - pkg-config only ultimately checks if all packages are removed so that errors can be reported when no other framework resolves packages).

Note that pkg-config need not be supported on all platforms. If you have a portable software, you'll probably need to handle it differently - possibly with the use of some specific framework.

-command

  • (target only)
  • (specific for type custom)

This is the exact command line that should be run when building this target. Meaningful only for custom targets.

-output

  • (target only)

It's meaningful for almost all types of target, as it simpy defines the list of files that is going to be produced by building the target. Usually to be specified explicitly for targets of type custom - for targets such as program or library it's generated automatically. It's important in case when the generated file is needed by another target to be completed

-depends

  • (target only)

This option defines dependency for the target, that is, the other target that must be completed prior to building this target. This is valid for all types of targets, but it's especially important for target of type phony, for which the dependencies are the only reason to exist (to have an "integrator" for multiple targets or a pass-thru dependency, that is, a library target that has to be a dependency for a program target, but the program should not be linked against that library)

-flags

  • (target only)

This accepts multiple "flags" in the form of simply a string of nonwhite characters. They are blindly passed to the makefile. This uses the feature of "flags" for Tclmake, see Tclmake documentation for details.

-rules

  • (target only)
  • (internally generated)

This is a key used mostly internally. There's no point in using it in any normal build definition, but it's mostly useful for frameworks. The normal processing facility puts here the rules generated for particular target according to its settings. For example, this is the place where the command line to call the compiler is put. There is a strict rule about the syntax - the element should consist of two things:

  • the target to be built
  • the rest of the rule specification (sources and the command line)

Currently the only supported generator is Tclmake, which means that the rule specification is suited for Tclmake. It doesn't mean that other generators can't be supported, it just means that this is a standard for all facilities. A support for other generators will likely be specified as simply translating this syntax of the rules into the desired syntax.

The usual way of adding rules is:

dict set rules $output_file "$sources {\n\t$command_line\n}"
ag $target -rules $rules

-depspec

  • (propagated from profile)

This defines the way how dependency files are being generated. Default is auto. Possible values are:

  • auto: Dependencies are pregenerated (dependency generation commands are executed during Makefile generation) and stored in the Makefile, meaning they are hardcoded until the next run. Note that it's not a perfect solution because the modification of include list must be manually tracked (you have to remember to regenerate Makefile if you changed the #include list in any of your source or header files). Moreover, this falls back to explicit if the gendep entry in your profile is empty (that is, there's no method that Silvercat can use to generate the dependency files).
  • explicit: No generation is done and you have to define header files used by source by ag-info SRCFILE -includes HEADER_FILES.
  • cached: This option generally makes use of dependency files that are generated for every source file separately and then used by Makefile. In worst case you have to rely on "gendep" profile entry, which is a command to generate the dependency file. If a compiler supports on-the-fly dependency generation, and there's a "depopt" profile entry set to appropriate option that causes it, then it is used. In this case the dependency file generation is done in the same step as when the file is being compiled. When "depopt" is not supported, then the dependency file is being generated in a separate command, basing on "gendep" profile entry.

-imgen

  • (propagated from profile)

This defines a transformation method used to translate the source filename into the name of any intermediate file resulting from that source file. There are the following methods supported:

  • name: the basename is the rootname (name without extension) of the source file
  • target-name: Like name, but prepended by the target name and dash
  • path: the basename is a transformed path to the source file relative to the current build directory. The transformation turns every path-separation slash into dash and every upward directory into underscore (so, ../src/dash.cc will get the basename _src-dash)
  • target-path: like path, but prepended by the target name and dash

You can freely define your own transformation method as well. You just need to define the following:

proc gen-imfile-MYGENERATIONNAME {target srcpath} { ... return the basename extracted how you wish ... }

Once you define this, you can use -imgen MYGENERATIONNAME. This name is used then for so-called object files, which will get the ".ag.o" suffix, as well as dependency files, which will get ".ag.dep" suffix.

-runon

  • (target only)

This is generally to decide if the target should be added to the overall all target or not. The only meaningful value is demand, which means that the target will not be added to a generated all target. For any other value, including when not defined, the target will be added to all.

Target types

The value passed to -type key is the most important for the target because defines what type of processing the target will undergo.

The default value for the -type key depends on some other keys already passed, that is:

  • if -command given, the target type is custom
  • otherwise, if both -source and -install are used, the target type is data
  • otherwise the target type is phony

It's recommended, however, that you don't rely on the guessed type and pass it explicitly using one of the following types:

program

This resolves to creating an executable file basing on source files and possibly additional libraries. For languages such as C or C++, it means compiling source files into object files and then linking them together, possibly with the use of additional libraries, into an executable file. The typical installation category is bin, which usually means $agv::prefix/bin directory. Some languages don't use separate source compilation and just compile all files at once in one step (Java, D).

If the program has a library target in its -depends list, this will be treated as a request to link against this library. If this is not you want, create an intermediate target of type phony, which will have this library as dependency - this way the rules will be still bound by dependencies, but the library will not be added.

Please note, of course, that the linkage will be done no matter if the library is going to be installed or not. In the case when a program is going to be linked against a dynamic library - which is a preferred way in case when both dynamic and static libraries were requested - then the linkage specification for the executable file will be added as dynamic. There's a potential user error, if an installable executable program is going to be linked against a non-installable dynamic library.

library

This resolves to compiling source files into object files and then produce a library out of that, possibly with additional libraries. By default the library specification is "static": the goal with the library name makes the static library and the appropriate header and static library installation goals will be added to the rules. Specifications for -libspec are additive and possible values are:

  • static, as mentioned already
  • noinst-static: same as static, but it won't be added to installation goal
  • dynamic: creates a dynamic library

The library will always get generated the installation target "TARGETNAME-install" and additionally targets with this name plus suffixes:

  • -devel: Installs header files. When static is present, install also static library
  • -runtime: Installs the dynamic library

That is, for example, if your library is named "iber", it will generate install-iber-devel and install-iber-runtime goals. When any of files required to be installed (headers or static library for -devel, or likewise dynamic library for -runtime), there will be just possibly nothing to install, but the goal rules will still be generated.

Like for program, if there is a target of type library in the -depends list, this library will be linked against this library. Note that some details of it may be platform-dependent. For example, in POSIX systems this means that the dynamic library, should one be created, will be linked against that library (this has nothing to do for static, as it doesn't have a dependency specification).

object

This defines compiling a source file into an object file, that is, intermediate ingredient that should be linked together with others to produce a program or a library. There's accepted exactly one file in -sources key. This shall be generally treated as something similar to library. It is useful if you want to compile one single source file with different flags than those applied for the other source files. In order to use this solution, however, you should treat this target as a special kind of static library, that is, that source file should be used to create this target, and the program, which's this source file would normally be an ingredient, should get this target as in its -depends. It can also be a dependency of a static library, in which case - in contrast to a dependency in a form of a static library - it will be directly added to the archive list of the library.

custom

This is more-less the same thing as defining the goal rule in the Makefile directly. The -command option should contain the exact command that will be put into the command statements in the rule generated for the Makefile. Of course, for this kind of target still the -depends option is honored, which will turn into extracted output files for these dependent targets.

Specific keys for this target are -sources, which defines the files to be read by the given command and -output, which defines the files to be produced by this command as output.

If any target is dependent on a custom target, the dependency will be also automatically linked to its -output files.

phony

This goal does nothing and the only reason of using it is with the -depends option. Don't mistakenly use this type when declaring a target with -command option because it simply won't be executed.

data

This normally does nothing, like phony, but it honors files specified in the -source option. All these files will be installed in a directory specified for a category in the -install option. The -install option is practically obligatory because otherwise this target would do nothing.

More elaborated example

NOTE: UNDER CONSTRUCTION.

Let's state that we have a program that is using Google Protobuf to pass data messages to the receiver over ZeroMQ.

The Google Protobuf library comes with its own langauge files defined as *.proto, from which *.pb.cc and *.pb.h files have to be generated, provided that we want them for C++. We have to define somehow how to create a rule for that, preferably some automated way. The best thing to do is to make a framework for that.

What this framework should do is to:

  • remove the *.proto files from -sources list
  • create rules for *.pb.cc and *.pb.h files generated from *.proto file
  • add *.pb.cc file to -sources list and *.pb.h to -headers list

Let's try:

namespace eval google {
    namespace eval protobuf {
        # Simplified version: find first available
        set protoc [auto_execok protoc]
        set outdir $::agv::builddir

        proc compile-pre {target sources} {
            variable protoc
            variable outdir

            set rules ""
            set extra_headers ""
            set out_sources ""  ;# will be rewritten
            foreach f $sources {
                if { ![string match *.proto $f] } {
                    lappend out_sources $f ;# leave it in -sources
                    continue
                }
                set name [file rootname $f]

                dict set rules $outdir/${name}.pb.h "$f {\n\t$protoc --cpp_out=$outdir $f\n}"
                lappend extra_headers $outdir/${name}.pb.h

                # let *.cc rule simply link to the *.h rule
                dict set rules $outdir/${name}.pb.cc "$f {\n\r!link $outdir/${name}.pb.h\n}"
                lappend out_sources $outdir/${name}.pb.cc
            }

            # Ok, now add the rules to the target's rules and
            # let the normal processing facility create rules for *.cc files.
            ::ag $target -rules $rules -sources {= $out_sources} -headers $extra_headers

            # Additionally set pkg-config package automatically
            ::ag $target -packages protobuf
        }
    }
}

Well, this is quite elaborated (I know the equivalence in qmake would be much shoter), but this at least is a normal procedure in a programming language. And it's universal and reusable. All you have to do is to make the above framework code somehow sourced in your Makefile.ag file and declared.

Ok, so now let's state that we have the following source files:

  • main.cpp (main entry)
  • sender.cpp (translates internal structure to protobuf message and sends it)
  • sender.h (used by main.cpp)
  • exchange.proto (Google Protobuf file)

And we have also an "engine" directory that should compile into a library named "engine" (libengine.a on POSIX):

  • engine/engine.cpp
  • engine/worker.cpp
  • engine/engine.h

Ok, so the main Makefile.ag file will contain the following statements:

# Source files (note that protobuf in -fw automatically adds protobuf package)
ag sender -fw google::protobuf -s main.cpp sender.cpp exchange.proto

# Zeromq package has to be added explicitly
ag sender -packages zeromq

# Add the directory with a separate library
ag-subdir engine
ag sender -depends engine/engine

And, well, that's all for this Makefile.ag. Although as we have this "engine" in a separate directory, we have to define Makefile.ag in that directory, which contains:

ag engine -type library -s engine.cpp worker.cpp

And that's generally all.

And... where are the header files? Nowhere. They are automatically extracted by the dependency extractor. Of course, if your profile doesn't define it, they will not appear in the generated rules and you'd have to add it manually, e.g.:

ag-info main.cpp -includes sender.h

And that's all you can imagine that would have to be added there.

Not too much elaborate? Well, I tried to make something really complicated, but somehow everything results in being easy in Silvercat! :P