[QBS] RFC: Deployment support for qbs
Christian Kandeler
christian.kandeler at nokia.com
Tue Feb 21 16:39:21 CET 2012
Hi,
in the last couple of days, I've thought a bit about what deployment
support could look like in the Qt Build Suite. Here's what I've come up
with. The following description is meant to be conceptually complete and
provides concrete examples. It might therefore appear a bit lengthy.
Bear with me.
1) Defining "Deployment"
Deployment can be viewed as
a) the last part of the build process or
b) as a dedicated process that follows after building.
The distinction is not academic, as we will see below.
In the simplest model, deployment is just a "make install" that puts
binaries and/or other resources from your project into the local file
system. However, while this might cover your needs for some project that
is supposed to be executed on your local machine, it is not enough if
files are to be deployed to other machines, e.g. for distributing your
software or, in the case of cross-compilation, even just testing.
Therefore, we split the concept of "Deployment" into two sub-concepts,
which we will call "Local Installation" and "Remote Deployment".
2) The example project
Let's assume our project is a simple library consisting of one source
and one header file. There is also a small test application that uses
the library's services. Without any deployment support, the project
could look like this:
import qbs.base 1.0
Project {
Product {
type: "dynamiclibrary"
name: "mylib"
Depends { name: "cpp" }
files: [ "mylib.h", "mylib.cpp" ]
}
Product {
type: "application"
name: "mylibTestApp"
Depends { name: "cpp" }
files: [ "main.cpp" ]
}
}
3) "Local Installation" in qbs
This concept exists already: Products can have the "installed_content"
property, which enables them to install files. This happens as part of
the build process, i.e. qbs currently implements definition a) of the
deployment term.
In our example above, you would want to install the header file and the
shared object, because they are needed to compile and link dependent
applications, respectively. You would not want to install the test app,
because it is not needed globally - you can just run it from the build
directory (or, in the case of cross-compilation, you cannot even run it
locally at all). So the library product now looks like the following,
while the rest of the project stays the same:
Product {
type: [ "dynamiclibrary", "installed_content" ]
name: "mylib"
Depends { name: "cpp" }
files: "mylib.cpp"
Group {
qbs.installDir: "/usr/lib"
files: target
fileTags: "install"
}
Group {
qbs.installDir: "/usr/include"
files: "mylib.h"
fileTags: [ "hpp", "install" ]
}
}
Note that in the case of cross-compilation, "qbs.installDir" will be
interpreted as being relative to a sysroot, which is a directory
specified in the platform description. The same directory is also used
when building the project, e.g. for gcc's '--sysroot' option.
Issues with the above approach:
- The "target" keyword used above does not yet exist. Currently
there is no way to refer to the build result of a product.
- The fact that the installation is part of the build process means
that if files are supposed to be installed to "global" locations in the
host file system, the user is forced to build the project as root. If
the install step were separate, the user could build normally and
afterwards do "sudo qbs install".
4) "Remote Deployment" in qbs
There is an important difference between local installation and remote
deployment: While it is rather unambiguous what the former means, the
latter can have very different variations. For our example project,
remote deployment could mean:
- Building a Debian package, copying the package to a device and
installing it there (e.g. the Harmattan use case).
- Copying the files into a directory tree that has been mounted via
a network file system, thereby copying it to a device.
- Copying (parts of) the files to a device via rsync.
- Building a tarball and uploading it to a web server for distribution.
- Putting the files into a self-extracting archive or some other
form of installer.
- ...
It seems hopeless to cover all these cases in qbs itself. However, what
qbs can do is to prepare the actual deployment in a sensible way.
4a) Specifying what to deploy
Just as for local installation, this can be done by introducing a
corresponding file tag. Let's assume you have cross-compiled the example
project and you now want to deploy it to some device for testing:
Product {
type: [ "dynamiclibrary", "installed_content" ]
name: "mylib"
Depends { name: "cpp" }
files: "mylib.cpp"
Group {
qbs.installDir: "/usr/lib"
files: target
fileTags: [ "install", "deploy" ]
}
Group {
qbs.installDir: "/usr/include"
files: "mylib.h"
fileTags: [ "hpp", "install" ]
}
}
Product {
type: "application"
name: "mylibTestApp"
Depends { name: "cpp" }
files: [ "main.cpp" ]
Group {
qbs.installDir: "/usr/bin"
files: target
fileTags: "deploy"
}
}
Note that in contrast to local installation, deployment does not include
the header file, since it is not needed on the device itself.
Conversely, it does include the test application, because it is run there.
However, this is not the only deployment case. When you are done with
testing, you want to distribute your library. If you intend to build a
"devel"-like package, you actually deploy the same files that you
install locally:
Product {
type: [ "dynamiclibrary", "installed_content" ]
name: "mylib"
Depends { name: "cpp" }
files: "mylib.cpp"
Group {
qbs.installDir: "/usr/lib"
files: target
fileTags: [ "install", "deploy" ]
}
Group {
qbs.installDir: "/usr/include"
files: "mylib.h"
fileTags: [ "hpp", "install", "deploy" ]
}
}
Product {
type: "application"
name: "mylibTestApp"
Depends { name: "cpp" }
files: [ "main.cpp" ]
}
You can support both cases via a property (does not work like this
currently, but should):
Project {
id: myProject
property bool testing: true
Product {
type: [ "dynamiclibrary", "installed_content" ]
qbs.installDir: "/tmp"
name: "mylib"
Depends { name: "cpp" }
files: "mylib.cpp"
Group {
qbs.installDir: "/usr/lib"
files: target
fileTags: [ "install", "deploy" ]
}
Group {
qbs.installDir: "/usr/include"
files: "mylib.h"
baseFileTags: [ "hpp", "install" ]
fileTags: myProject.testing ? baseFileTags :
baseFileTags.concat("deploy")
}
}
Product {
type: "application"
name: "mylibTestApp"
Depends { name: "cpp" }
files: [ "main.cpp" ]
Group {
qbs.installDir: "/usr/bin"
files: target
fileTags: myProject.testing ? "deploy" : []
}
}
}
4b) Deploying
As explained above, qbs cannot be responsible for the complete
deployment process, but it can do some preparation for common use cases.
For instance, packaging often involves copying the files to be packaged
into a directory structure mirroring the one on the target system. For
instance, the abovementioned "devel package" case for our example
project could look like this, if you are targetting a dpkg-based system:
$ qbs deploy myProject.testing:false project.deployRoot:debian/mylib
This would remove the contents of the given directory if it exists
already and then copy all files that have a "deploy" tag to their
respective "installDir"s, prepending the value of the "deployRoot"
property. In this example, you would then call dpkg-buildpackage in the
same directory. This is outside the scope of qbs, but it builds nicely
on qbs' work. (Alternatively, you would perhaps want to integrate qbs
into the dpkg-buildpackage flow via the rules file.)
Now let's assume that for testing, you do not want the overhead of
package creation. Instead, you just want to copy file by file to the
device using scp. In such a case, you don't need "qbs deploy", since
that would just be an unnecessary indirection. What you do need,
however, is the list of files to copy over, preferably in a simple,
easily parsable format:
$ qbs dump-deploy-info myProject.testing:true
For our example project, this would result in output similar to the
following:
/home/icke/myproject/build/debug/mylib.so|/usr/lib/mylib.so
/home/icke/myproject/build/debug/mylibTestApp|/usr/bin/mylibTestApp
That's it. Opinions?
Christian
More information about the Qbs
mailing list