15  Libraries

So far, we have explored the predefined libraries extensively and have illustrated the applications of many. While the language specifications make it a rich and robust platform, building real life applications need more. All successful languages are backed by active and rich library system incorporating a combination of software as well as documentation. Recent examples include the ecosystems of nodejs, golang, rust and so on.

A recent addition - within the last few years - supporting Ada is the ada library system https://alire.ada.dev. The repository is a rich source of software support - in numerous disciplines; serving as components of applications as well as examples of good software engineering practises. Use of this library and the component crates is supported by alr a key driver of Ada software development.

In this chapter, we will attempt to incorporate some of these crates into our own projects.

15.1 Learning Objectives

  • Utilizing a library crate from alire

  • Application revision management based on standard practises

  • Tracking application binaries to sources

15.2 Projectlet fileres

Wordgames such as https://gitlab.com/ada23/words.git depend on a dictionary resource. Other applications frequently utilize audio files, graphics data as resources. This projectlet is designed to convert the resource files to a component of the executable image so that at runtime, there is no reason to reach into the file system of the host. Any file is translated into Ada source code which in turn is incorporated in the application executable. The applications at runtime may choose to use these resources directly in memory or by creating temporary files on the host system.

Resources such as dictionaries which are primarily textual can benefit a lot from compression. This projectlet will then compress the file using the zlib_ada crate from alire.ada.dev. Since the data is compressed, at runtime the resource need to be decompressed before usage. The library supports both operations.

15.2.1 Getting started

Once the crate is chosen, incorporating it in a project is straightforward:

alr with zlib_ada

This directs alr to make appropriate modifications to add the zlib_ada to the project. The exact version of zlib_ada is recorded in the alire.toml file and the project config file is modified to include the reference to the crate:

head -5 ../../fileres/config/fileres_config.gpr
--  Configuration for fileres generated by Alire
with "zlib.gpr";
abstract project Fileres_Config is
   Crate_Version := "0.0.0";
   Crate_Name := "fileres";

The package then is used just like any other predefined library package:

~/bin/codemd ../../fileres/reslib/src/res-pack.adb -x Generate -l
0061 |         Ada.Streams.Stream_IO.Open
0062 |             (file,
0063 |             Ada.Streams.Stream_IO.In_File,
0064 |             name);
0065 |         stream := Ada.Streams.Stream_IO.Stream (file);
0066 |         declare
0067 |             buffer : Ada.Streams.Stream_Element_Array
0068 |                  (1 .. Ada.Streams.Stream_Element_Offset (filesize));
0069 |             bufferlen : Ada.Streams.Stream_Element_Offset;
0070 |         begin
0071 |             stream.Read (buffer, bufferlen);
0072 |             pragma Assert(Integer(bufferlen) = Integer(filesize) );
0073 |             Deflate_Init (Compressor);
0074 |             declare
0075 |                 compressed_data : Ada.Streams.Stream_Element_Array
0076 |                     (1 .. Ada.Streams.Stream_Element_Offset (filesize));
0077 |                 compressed_size : Stream_Element_Offset;
0078 |             begin
0079 |                 Translate (Compressor, buffer, I, Compressed_Data, compressed_size, Finish);
0080 |                 pragma Assert (I = Buffer'Last);
0081 |                 Close (Compressor);
0082 | 
0083 |                Set_Output( resfile.file.all );
0084 |                New_Line ;
0085 |                Put_Line( Ascii.HT & Sanitize(Simple_Name(name)) & " : res.ResourceType := ( ");
0086 |                Put_Line( Ascii.HT & Ascii.HT & "To_Unbounded_String( " & '"' & Simple_Name(name) & '"' & ") , ");
0087 |                Put_Line( Ascii.HT & Ascii.HT & filesize'Image & " , ");
0088 |                Put_Line( Ascii.HT & Ascii.HT & "To_Unbounded_String (");
0089 |                Hex.dump.Dump
0090 |                     (compressed_data'Address,
0091 |                     Integer (bufferlen),
0092 |                     false ,
0093 |                     true ,
0094 |                     32 ,
0095 |                     resfile.file.all );
0096 |                
0097 |                Put_Line( Ascii.HT & Ascii.HT & '"' & '"' &  " )) ;") ;
0098 |                Set_Output( Standard_Output );
0099 |             end;
0100 |         end ;
0101 |         Ada.Streams.Stream_IO.Close (file);

As shown above, the file of interest is read verbatim, compressed using zlib and written to a source file for runtime access. For example, the file res.ads is rendered as follows:

~/bin/codemd ../../fileres/test/demo_resources.ads.keep -x Output -l
0256 |  RES_ADS : res.ResourceType := ( 
0257 |      To_Unbounded_String( "res.ads") , 
0258 |       221 , 
0259 |      To_Unbounded_String (
0260 | "789c758db10ec2300c44f77ec57d413f804c48cc0c2dccc8246e8928496527aa" &
0261 | "e0eb6994aa1378f2bbb3ef169f1e383a6afb243e8cda5ec33de6e0d8c1202bff" &
0262 | "f64c33937dd2c81056786d00a4f7cce8586316cb97025517b6515cd9d619fcc4" &
0263 | "815e8c03f6b05b8d87d96e7aff29fe9952169a76f94489febe71705bd1ca1514" &
0264 | "e60beb6946d6000090009e0401000000d06c426b01000000dd00000000000000" &
0265 | "104aa50401000000b06e426b01000000b06d4202080000000f00000008000000" &
0266 | "00000000ffffffff886e426b01000000104aa50401000000dc00a0040f" &
0267 |      "" )) ;

As shown above, the original filename and its raw size is included along with the compressed blob of data for runtime comparison:

~/bin/codemd ../../fileres/test/demo.adb -x Unpack -l
0010 |    declare
0011 |       d : Ada.Streams.Stream_Element_Array := res.unpack.Unpack( demo_resources.RES_PACK_ADS );
0012 |       dstr : String( 1..d'Length ) ;
0013 |  for dstr'Address use d'Address ;
0014 |    begin
0015 |       Put_Line(dstr);
0016 |    end ;

The unpacking operation utilizes the zlib:

~/bin/codemd ../../fileres/reslib/src/res-unpack.adb -x Unpack -l
0043 |       Inflate_Init (Decompressor);
0044 |       loop
0045 |          Translate
0046 |             (Decompressor,
0047 |                resvalstr 
0048 |                (P + 1 .. 
0049 |                    Stream_Element_Offset'Min (P + Block_Size, L)) ,
0050 |                P,
0051 |                Uncompressed_Data
0052 |                  (Total_Out (Decompressor) + 1 .. Uncompressed_Data'Last),
0053 |                O,
0054 |                No_Flush);
0055 |                if verbose
0056 |                then
0057 |                Ada.Text_IO.Put_Line
0058 |                   ("Total in : " & zlib.Count'Image (Total_In (Decompressor)) &
0059 |                      ", out : " & zlib.Count'Image (Total_Out (Decompressor)));
0060 |                end if ;
0061 |                exit when Total_Out (Decompressor) = L;
0062 |       end loop;

At this point the application can utilize the decompressed data as needed either by creating a file or other processing. In the case of a dictionary, the data may be broken into individual words for inclusion in a more convenient, searchable data structure as:

~/bin/codemd ../../letters/boxed/src/dictionary.adb -x Dict -l
0037 |       declare
0038 |          dictbytes : Stream_Element_Array := res.unpack.Unpack( fileres );
0039 |          dictstr : String( 1..dictbytes'Length );
0040 |             for dictstr'Address use dictbytes'Address ;
0041 |       begin
0042 |          from := dictstr'First ;
0043 |          while from < dictstr'Last 
0044 |          loop
0045 |             if Is_Letter(dictstr(from) )
0046 |             then
0047 |                for to in from+1..dictstr'Last
0048 |                loop
0049 |                   if not Is_Letter( dictstr(to))
0050 |                   then
0051 |                      wordcount := wordcount + 1 ;
0052 |                      Words_Pkg.Insert( result , To_Unbounded_String(dictstr(from..to-1))) ;
0053 |                      --Put_Line(dictstr(from..to-1));
0054 |                      from := to ;
0055 |                      exit ;
0056 |                   end if ;
0057 |                end loop ;
0058 |             else
0059 |                from := from + 1 ;
0060 |             end if;
0061 |          end loop  ;
0062 |          Put("Dictionary contains "); Put(Integer(Words_Pkg.Length(result))); Put_Line(" entries");
0063 |       end ;

15.3 Projectlet gitrev

The focus of this projectlet is to enable traceability from the binary to the source code. git being the predominant source code repository system, this projectlet attempts to capture enough tracking details to embed into the executable enabling querying and reporting of the exact set of sources that contributed to the binary. This leverages the commitid maintained by git regardless of the backend e.g. GitHub, GitLab, Bitbucket or whatever.

15.3.1 Git interaction

Using the git command line, we can obtain the relevant info. In this projectlet, we start by locating the executable:

~/bin/codemd ../../toolkit/adalib/src/git.adb -x Locate -l
0016 |    fullgit : constant GNAT.os_lib.String_Access := Locate_Exec_On_Path ("git");

For each query, the executable is invoked with appropriate command arguments in the appropriate directory (where the repository had been cloned to then parse the output as required:

~/bin/codemd ../../toolkit/adalib/src/git.adb -x Exec -l
0066 |    -- Execute the command in the specified directory and return the output
0067 |    function Exec (dir : string; cmd : String) return String is
0068 |       cwd     : constant String := Ada.Directories.Current_Directory;
0069 |       status  : aliased Integer;
0070 |       arglist : constant Argument_List_Access := Argument_String_To_List (cmd);
0071 |    begin
0072 |       Ada.Directories.Set_Directory (dir);
0073 |       declare
0074 |          result : constant string :=
0075 |            GNAT.Expect.Get_Command_Output
0076 |              (fullgit.all, arglist.all, "", Status'Access, Err_To_Out => True);
0077 |       begin
0078 |          if Verbose then
0079 |             Put ("Dir: ");
0080 |             Put (dir);
0081 |             Put (" cmd: ");
0082 |             Put_Line (cmd);
0083 |             Put_Line (result);
0084 |          end if;
0085 |          Ada.Directories.Set_Directory (cwd);
0086 |          return result;
0087 |       end;
0088 |    exception
0089 |       when others =>
0090 |          Put ("Exception executing ");
0091 |          Put (cmd);
0092 |          Put (" @ dir ");
0093 |          Put_Line (dir);
0094 |          return "";
0095 |    end Exec;

While most of the time, the value printed is just what is needed, sometimes there are multiple lines of output eg. when branches are listed. We may have to process the output line before extracting the desired value by breaking the output into individual lines:

~/bin/codemd ../../toolkit/adalib/src/git.adb -x CurrentBranch -l
0169 |   function CurrentBranch (dir : String := ".") return String is
0170 |       cmd : String := "rev-parse --abbrev-ref HEAD" ;
0171 |    begin
0172 |       return Exec( dir , cmd );
0173 |    end CurrentBranch ;

on the other hand:

~/bin/codemd ../../toolkit/adalib/src/git.adb -x Tags -l
0244 |    function Tags (dir : String := ".") return wordlistpkg.Vector is
0245 |       taglines : constant String             := Exec (dir, "tag --list");
0246 |       result   : constant wordlistPkg.Vector := Get_Lines (taglines);
0247 |    begin
0248 |       return result;
0249 |    end Tags;

With this support generating a source file - just like fileres above that can be incorporated in the executable is an easy next step:

cat ../../toolkit/examples/gitrev/src/revisions.ads
--------------------------------------------
-- Created 2025-01-20 10:40:22
--------------------------------------------
package revisions is
    dir : constant String := "/Users/rajasrinivasan/Prj/GitLab/toolkit/examples/gitrev" ;
    version : constant String := "0.0.2" ;
    repo : constant String := "git@gitlab.com:ada23/toolkit.git" ;
    commitid : constant String := "bbc1d79bdac1eacd0c3fd7cac22dd01022eff956" ;
    abbrev_commitid : constant String := "bbc1d79" ;
    branch : constant String := "main" ;
end revisions ;

15.3.2 Version Tracking

Another key component is version tracking and the standard to adopt is https://semver.org/. alire of course provides appropriate support which can be incorporated in an application similar to zlib_ada above:

alr with semantic_versioning

The application gitrev makes a very light use of semantic versioning - insisting that the user provided revision string conforms to the standard format. Internally gitrev itself attempts to use semantic versioning:

~/bin/codemd ../../toolkit/examples/gitrev/src/gitrev.adb -x semver -l
0046 |    declare
0047 |       vstring : constant String := 
0048 |                Semantic_Versioning.Image(
0049 |                Semantic_Versioning.Parse( cli.version.all ) ) ;
0050 |       argdir       : constant String := To_String (dir);
0051 |       specfilename : constant String := cli.outputFile.all;
0052 |       specfile     : File_Type;
0053 |    begin
0054 |       Create (specfile, Out_File, specfilename & ".ads");
0055 |       Set_Output (specfile);
0056 |       Put_Line (longcomment);
0057 |       Put (comment);
0058 |       Put ("Created ");
0059 |       Put_Line (Local_Image (Clock));
0060 |       Put_Line (longcomment);
0061 |       Put ("package ");
0062 |       Put (specfilename);
0063 |       Put_Line (" is");
0064 |       StringConstOutput ("dir", Full_Name (argdir));
0065 |       New_Line;
0066 |       StringConstOutput ("version", vstring );
0067 |       New_Line;

and at runtime:

~/bin/gitrev -v
Command Line Arguments
Verbose TRUE
Output File revisions
Version 0.0.1
gitrev version
Repo git@gitlab.com:ada23/toolkit.git
Version 0.0.2
Commit Id bbc1d79bdac1eacd0c3fd7cac22dd01022eff956
Please provide a dir. Use '.' for current

15.4 Stretch

  • fileres is very much Ada centric keeping up with the spirit of this book. Resources are of course need in applications regardless of programming language. Extending this to a scripting language such as python may be quite interesting.

  • There are more featureful crates in the alire repository to perform such resource embedding. fileres could be extended to achieve feature parity.

  • gitrev and supporting libraries potentially can be extended to build an automatic build system like https://www.jenkins.io.

  • An automatic build system that generates build numbers for the semantic versioning scheme outlined above will be quite useful.

15.5 Sample Implementation

Repository: fileres 
Repository: toolkit
Directory: example/gitrev