Capri, a script based build system

This forums is for OS project announcements including project openings, new releases, update notices, test requests, and job openings (both paying and volunteer).
Post Reply
User avatar
max
Member
Member
Posts: 616
Joined: Mon Mar 05, 2012 11:23 am
Libera.chat IRC: maxdev
Location: Germany
Contact:

Capri, a script based build system

Post by max »

Note: this description is quite outdated, I will add a link to the project website once it's online.

Heyho:)

I'd like to announce a project I've been developing lately. It's a script based build system named Capri written in C++. Initially I just made it to have something for my kernel, but I think it could be useful for other people, too. I'll explain a little about what you can do with it here. By now it doesn't have a project website, but if I see enough interest I'll put the effort in it and make one (with an extensive documentation, for sure). Until then, you will find news and more in this thread.

It's quite too much to tell about all the features in a single post, but I want to give you an idea and maybe wake your interest. Feel free to ask any questions. So, let's dive right in with some examples! ;)


Projects & Tasks

First things first, the main thing that you write when using Capri is a project. A build script can contain multiple projects, and each project can contain multiple tasks. A task can have dependencies, which are executed before the task itself is run. This is an example for a minimum "Hello world" with a dependency:

Code: Select all

import "SomeOtherScript.capri";

project Example default all {

    task all depends clean {
        Utils.println("Hello world!");
    }

    task clean {
        Utils.println("Doing cleanup...");
    }
}
A dependency can not only be the name of a task, but any kind of expression: you can for example call a dependency with a parameter of the task:

Code: Select all

task first(par) depends second(par) { ...
task someTask(test, bla) depends anotherTask(test + "/bin", 43 * bla) { ...
This allows writing dependencies chains and reusing tasks by passing parameters.

Script & syntax

The language itself has a quite easy syntax (similar to Java/C++/JavaScript). You don't have to define variables, you just assign them. If you want to define a variable in a context higher than where you're assigning it, you must use the keyword def.

Code: Select all

def myVariable;
for(i = 0; i < 100; i++) {
    myVariable = 25 + i;
}
// now you can still access myVariable
The script also allows using an expression within a string literal, what makes it much easier to concatenate strings:

Code: Select all

myFlags = "-std=c++11";
foreach(source : sources) {
    System.execute("gcc {myFlags} {source} -o {File.getFileName(source)}.o");
}

// You could also call a function from within that:
Utils.println("Hello {getName()}!");
To define an array, you can simply write expressions inside {"curly", "brackets"}.

Note: anything you pass to a function or assign is copied, there is not pass-by-reference.


Native functions

So, we have a fancy script now, but we also want to do things with it. The script by now provides the following native functions (which can easily be extended, see the "Main.cpp"), I think you can figure out what most of them do :)
  • System.execute(command)
  • String.endsWith(str, suffix)
  • File.isFolder(path)
  • File.isFile(path)
  • File.cleanFolder(path)
  • File.listFolder(path)
  • File.listTree(path) - recursive file system listing
  • File.createFolder(path)
  • File.deleteFile(path)
  • File.deleteFolder(path)
  • File.getFileName(path)
  • File.getAbsolutePath(path)
  • File.changeDirectory(path)
  • File.getCurrentDirectory()
  • Utils.print(message)
  • Utils.println(message)
  • Utils.length(obj) - returns the length of the array or string you give to it
Distinguishing architectures/systems/targets

This is the way to distinguish between operating systems, architectures or later targets (not implemented yet). The values for these switches are set in the "Main.cpp".

Code: Select all

compiler = "gcc";

task prepare {
    architecture x86 {
        compiler = "arm-gcc"; // we want to cross-compile
    }
    system windows {
        flags += "-someSpecialWindowsFlag";
    }
}
This allows for example to create a global variable and then assign something special if you're on a specific architecture.



Concurrency

The script is also able to perform things concurrently. You can do that like this:

Code: Select all

executionA = concurrent someFunction(withSomeParameter);
executionB = concurrent someOtherFunction();

// These two functions will now execute simultaneously.
// Before the current context exits, they will automatically be joined,
// but you can also join them manually to get the result:

resultA = join executionA;
resultB = join executionB;
You can put the concurrent keyword in front of any statement, but note that if you put it in front of for example a while-loop, the entire loop is run in a thread, and not each execution of the body. This is something I'll add in the near future, but maybe with a different keyword.



Launching it

Once you have compiled Capri and added the binary to your PATH, you can easily run it like this:

Code: Select all

capri
Simple, isn't it? It will automatically look for a file named "Build.capri" and execute it's default task. If you want to run a specific task, or any other expression, you can do this:

Code: Select all

capri specialTask
// or use any kind of expression:
capri performMyTask("Some text!")
The expressions you pass there will then be interpreted within the context of the first project found in "Build.capri".
If you want to load a different file than the main script, you must do it like this (this isn't very comfortable yet, because you can't use the default task, comes on the TODO-list):

Code: Select all

capri all MyScript.capri

How can I use this for my project?

By now it's still in an early stage, however you can play around and already create a fully functional buildscript. This is a little example file that you can use:

Code: Select all

project MyProject default all {
    
    isoPath = "image.iso";
    srcPath = "src";
    binPath = "bin";
    
    task all depends compile, link {
        Utils.println("Finished, running...");
        System.execute("qemu-system-i386 -cdrom {isoPath}");
    }

    task compile {
        allFiles = File.listTree(srcPath);
        
        foreach(file : allFiles) {
            System.execute("i386-elf-gcc {file} -o {binPath}/{File.getFileName(file)}.o ...");
        }
    }

    task link {
        System.execute("i386-elf-ld {binPath}/*.o ...");
    }
}
To get some inspiration, you can find the buildscript of my kernel here.

Getting the source

It might not have the very best code yet and gives some weird error messages if you mess up the syntax. ^^ But its only a work-in-progress by now.

// Out of date, sorry ;)

Feel free to ask questions! If you are interested I would be glad to hear some feedback on this, considerations, critics and suggestions are highly appreciated. :)

Thank you! :)
Greets, Max
Last edited by max on Thu Oct 02, 2014 1:03 pm, edited 2 times in total.
Kevin
Member
Member
Posts: 1071
Joined: Sun Feb 01, 2009 6:11 am
Location: Germany
Contact:

Re: Capri, a script based build system

Post by Kevin »

I don't want to sound negative, but... I missed the description of the problem that it solves. ;)

To me it looks mostly like Makefiles with a different syntax and with a tendency to use more of a procedural than a declarative style. Is it just that and you use it just because you can, or does it help you to overcome any limitations of make?
Developer of tyndur - community OS of Lowlevel (German)
User avatar
max
Member
Member
Posts: 616
Joined: Mon Mar 05, 2012 11:23 am
Libera.chat IRC: maxdev
Location: Germany
Contact:

Re: Capri, a script based build system

Post by max »

Kevin wrote:I don't want to sound negative, but... I missed the description of the problem that it solves. ;)

To me it looks mostly like Makefiles with a different syntax and with a tendency to use more of a procedural than a declarative style. Is it just that and you use it just because you can, or does it help you to overcome any limitations of make?
Hey Kevin, hehe you dont :)
I think that the syntax of Makefiles is much too complicated and hard to understand, especially if you have a really big one, with recursive source directories and the like (no, imho autotools dont make it beautiful in no way^^)
Also, Capri allows scripts to be platform-independent, a new platform can be added by just implementing the native stubs (and having a C++lib :P) and then you can use things like File.copy without worrying. I want to make this new, fresh and easy but still as reliable and flexible as Makefiles.
Post Reply