Technical details on how fbs is meant to be used.
fbs is a Python-based build tool for desktop applications that use PyQt or PySide. It takes your source code and turns it into a standalone executable on Windows, Mac or Linux. It also lets you create an installer for your app.
The best place to get started with fbs is the 15 minute tutorial. If you haven't already taken it, it is highly recommended you do so. This Manual is meant as the next, more detailed resource.
fbs runs on Windows, macOS and Linux (Ubuntu, Arch or Fedora). You also need Python 3. The free version of fbs supports Python 3.5 and 3.6. Later Python versions require fbs Pro.
The easiest (and non-intrusive) way of installing fbs is via pip and a Python virtual environment. To create a virtual environment in the current directory, execute the following command:
python3 -m venv venv
Then, activate the environment with one of the commands below:
# On Mac/Linux: source venv/bin/activate # On Windows: call venv\scripts\activate.bat
Next, use pip
to install fbs and its
dependencies:
pip install fbs PyQt6
You can similarly install PySide6
,
PyQt5
or PySide2
. Using PyQt6 or
PySide6 requires
fbs Pro.
Your main point of contact with fbs will likely be its command line. For instance, the command
fbs freeze
turns your application into a standalone executable.
Other available commands are startproject
,
run
, clean
and
installer
.
Run fbs
(without further arguments) to see
their descriptions. The
tutorial
shows them in action.
The easiest way to start an fbs project is via the following command:
fbs startproject
This prompts you for a few values:
Once you have entered them, fbs creates a skeleton project
in the src/
folder of the current directory.
fbs projects use the following directory structure.
Parentheses (...)
indicate that a file is
optional.
src/ |
Root directory for your source files |
build/ |
Files for the build process |
settings/ |
Build settings: |
base.json |
all platforms |
(mac.json) |
specific to Mac |
(windows.json) |
... |
(linux.json) |
all Linux distributions |
(arch.json) |
specific to Arch Linux |
(fedora.json) |
... |
(ubuntu.json) |
... |
(release.json) |
during a release |
main/ |
Implementation of your app |
icons/ |
Your app's icon. |
python/ |
Python code for your application |
(resources/) |
Data files, images etc. See below. |
(freeze/) |
Files for freezing your app |
(installer/) |
Installer source files |
(windows/) |
|
(mac/) |
|
... |
|
(requirements/) |
Your Python dependencies: |
(base.txt) |
on all platforms |
(linux.txt) |
on all Linux distributions |
(arch.txt) |
on Arch Linux |
... | ... |
As you use fbs, you will see that it generates output in a
folder called target/
, next to the above
directories. It may also create a folder called
cache/
, which you can delete whenever you want.
Finally, another typical (but not required) folder on
this level is venv/
.
The command fbs run
is great to quickly run
your app. Many people however prefer working in an IDE such
as PyCharm.
It especially simplifies debugging.
To run an fbs app from other environments (such as an IDE, or the command line), you simply
src/main/python
on your
PYTHONPATH
and
src/main/python/main.py
.So for example on Mac and Linux, you can also run your app from the command line via
PYTHONPATH=src/main/python python src/main/python/main.py
(assuming the virtual environment is active).
Here are screenshots of how PyCharm can be configured for this:
In order for fbs to find it, your Python source code must
lie in src/main/python/
. There, you need a
script that starts your app with an
ApplicationContext
.
The default script that is generated at
src/main/python/main.py
when you run
fbs startproject
looks as follows:
from fbs_runtime.application_context.PyQt6 import ApplicationContext from PyQt6.QtWidgets import QMainWindow import sys if __name__ == '__main__': appctxt = ApplicationContext() # 1. Instantiate ApplicationContext window = QMainWindow() window.resize(250, 150) window.show() exit_code = appctxt.app.exec() # 2. Invoke appctxt.app.exec() sys.exit(exit_code)
The steps 1. and 2. are the minimum of what's required for fbs to work.
As your application grows in complexity, you will likely
want to split its source code across multiple files. In this
case, it is recommend that you place them all inside one
package. For instance, if your app is called My App then the
package could be called my_app
and the
directory structure could look as follows:
src/main/python/
my_app/
__init__.py
main.py
module_a.py
module_b.py
If main.py
is again the script that
instantiates the application context, then you need to
change the main_module
setting in
base.json
to "src/main/python/my_app/main.py"
. This lets
fbs know about the new location.
A final tip for more complicated applications: Check out
@cached_property
.
Together with
ApplicationContext
,
it can really help you wire up the components of your app.
Most applications will use extra libraries to implement
their functionality. In the typical case, this is as simple
as pip install library
on the
command line and import library
in
your Python code. When fbs sees the import statement, it
automatically packages the dependency alongside your
application.
Sometimes, it can happen that automatic packaging does not
work for a particular library. That is,
fbs run
works but running the output of
fbs freeze
fails. A common symptom of this on
Windows is the following dialog when you try to run your
frozen app:
To debug this, add the --debug
flag to freeze:
fbs freeze --debug
When you then start your frozen app from the command line,
you should get some debug output. If you see an
ImportError
mentioning a module
xyz
, add the following to
src/build/settings/base.json
:
{ ... , "hidden_imports": ["xyz"] }
If this does not help to fix a dependency problem, please search online for "<your library> PyInstaller". (fbs uses PyInstaller to package dependencies.) If that doesn't turn anything up, you may be able to get help on PyInstaller's issue tracker.
Once you are using a new library in your project, it is
recommended that you add it to the
requirements/
folder. For example, say
you are on Windows and have added the Windows-only
dependency somelibrary
. Then you would create
the requirements/
folder next to
src/
, copy
base.txt
into it and also create the following file
windows.txt
there:
-r base.txt PyQt6 somelibrary==1.2.3
The advantage this has is that other developers who check out your repository from version control can then quickly get all required Python libraries via the command:
pip install -r requirements/windows.txt
Further, some fbs commands such as
buildvm
can only take
your dependencies into account if you follow the above
structure.
Applications often require data files beyond their source
code. Typical examples of this are images displayed in your
application. Others are .qss
files, Qt's
equivalent of CSS.
fbs makes it very easy for you to include data files.
Simply place them in one of the following subfolders of
src/main/resources/
:
base/
for files required on all OSswindows/
for files only required on Windows
mac/
...linux/
...arch/
for files only required on Arch Linux
fedora/
...ubuntu/
...
When you call fbs freeze
, fbs automatically
copies the applicable files into your app's frozen directory
inside the target/
folder.
To access a resource file from your Python code, simply call
ApplicationContext#get_resource(...)
.
The tutorial gives an
example
of this.
In addition to the above, there are other directories you can use to include extra files.
Files in src/freeze/mac/
, ...
are
only included in the frozen version of your app. (So they're
not available when you do fbs run
.) Their
canonical example is
Info.plist
,
a meta file that tells macOS about the name of your
application, its version etc.
The folders src/installer/windows/
,
...
contain files for your installer on the
various platforms. For example: The file
src/installer/windows/Installer.nsi
contains the implementation of the Windows installer.
Consider
Info.plist
mentioned above. It contains the following lines:
... <key>CFBundleExecutable</key> <string>${app_name}</string> ...
Where does ${app_name}
come from? The answer is
resource filtering: As fbs copies Info.plist
from
src/
to target/...
, it replaces
${...}
by the corresponding setting. For the
tutorial, app_name
is defined in
base.json
as follows:
{ "app_name": "Tutorial", ... }
So, the Info.plist
that ends up in
target/
contains Tutorial
and not
${app_name}
.
To prevent unwanted replacements (eg. in image files),
resource filtering is only applied to files listed in the
setting files_to_filter
. See the
file
mac.json
for an example.
A limitation of resource filtering is that it is not applied
during fbs run
. In this case, the files you
obtain from
ApplicationContext#get_resource(...)
contain the placeholders unchanged.
The above descriptions and the tutorial lead up to the creation of an installer. But publishing a production grade app requires several more steps:
fbs accomplishes all of the above tasks when you run the following command:
fbs release
However, this command requires a few preparations.
The first step is to install a few more dependencies. To do this, please execute:
pip install fbs[upload]
Next, create an fbs account via the command:
fbs register
Alternatively, if you already have an account, you can use
fbs login
to set its credentials.
The remaining preparations depend on your target operating system. They are discussed separately below.
Automatic updates are not yet implemented on Windows. If you
want to code sign your application, please see the
relevant section.
Otherwise, no special setup is required. Just run
fbs release
to publish your app.
fbs does not yet implement either code signing or automatic
updates on Mac, so no special setup is required. Simply
execute fbs release
to publish your app.
Unlike Windows, there is one thing to take into account when publishing your app: Try to build it on as old a version of macOS as possible. This improves the compatibility of your app with older versions of macOS. For example, an app built on macOS 10.10 is most likely to also run on 10.14, but not the other way around. Most people use virtual machines to run old versions of macOS.
Unlike on the other platforms, fbs fully implements code signing and automatic updates on Linux. The following gives you a minimal but complete example:
fbs startproject fbs gengpgkey fbs register # or `login` if you already have an account fbs buildvm ubuntu # or `arch` / `fedora` fbs runvm ubuntu # In the Ubuntu virtual machine: fbs release
After the above, users on Ubuntu can install your app with the following commands:
wget -qO - https://fbs.sh/<user>/<app>/public-key.gpg | sudo apt-key add - echo 'deb [arch=amd64] https://fbs.sh/<user>/<app>/deb stable main' \ | sudo tee /etc/apt/sources.list.d/<app>.list sudo apt-get update sudo apt-get install <app>
What's more, users who install your app in this way automatically receive updated versions through their native package manager.
You can infer from the gengpgkey
command above
that fbs uses a GPG key. This is the standard for
code signing on Linux.
The next command, register
, lets you create an
account for fbs's backend. This is required so only you can
modify your app.
buildvm
and runvm
commands
Apps built on Ubuntu 16 usually run on Ubuntu 18, but not
the other way around. The buildvm
and
runvm
commands let you create and start virtual
machines running older Linux versions. For example,
runvm ubuntu
starts a virtual machine
running Ubuntu 16. Building your app there maximises its
compatibility.
Using virtual machines for releasing your app has another benefit: The VM serves as an isolated environment. This makes your builds more reproducible. Also, it prevents you from having to install tools that are only required during a release. And from having to import GPG keys.
The two VM commands are implemented using Docker. You can
see this when you run docker images
. For
example, after buildvm ubuntu
:
michael:~$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE myapp/ubuntu latest 30abafe515e8 19 hours ago 1.03GB
The command buildvm ubuntu
builds a Docker
image according to the instructions in
src/build/docker/ubuntu/Dockerfile
. The
default implementation
performs the following steps:
venv/
directory.
requirements/
.
When you then do runvm ubuntu
, fbs mounts the
files from your project directory inside the container and
starts it. Because your files are mounted, any outside
changes you make to eg. src/...
are immediately
visible. So eg. freeze
always uses the current
version of your source code, even when run in a container.
On the other hand, some changes are not immediately visible
in the container. For example, the virtual environment is
only updated when you call buildvm
. So you need
to re-run this command after
adding Python dependencies.
Similarly for when you set or change the GPG key.
Because of the way Docker works, runvm
always
starts from the state created by buildvm
. This
means that any changes you make inside runvm
are lost as soon as you type exit
. So while it
may be tempting to call pip install somelibrary
inside the container, the results of this command will be
short-lived. As mentioned
above, you need to add
the dependency to
requirements/
instead.
The final caveat applies to the folders venv/
and target/
inside the Docker container. Unlike
other project files such as src/
, these two
directories are not just mounted into the container. The
target/
folder actually points to
target/ubuntu
(or .../arch
etc.)
in your project directory. And venv/
does not
exist outside the Docker container at all.
fbs lets (and in fact encourages) you to release your app on multiple operating systems. The caveat is that fbs commands only ever target the current platform. That is, you for instance cannot create a Mac installer when running fbs on Windows. The solution to this is to use virtual machines to invoke fbs on different platforms. A video of fbs's creator shows examples of this in practice.
Another recent cool solution is to use GitHub Actions to automate the workflow for all OSs. Please see this repository by J. F. Zhang. A caveat is that if you are using fbs Pro, then you should only do this from a private GitHub repository. Otherwise, the whole world would be able to obtain your Pro credentials, which quickly gets them blocked.
Code signing is required to avoid warnings by the user's OS that your app is untrusted:
For code signing on Linux, see the section on releasing for Linux above. On macOS, fbs does not (yet) implement code signing. For instructions on Windows, see below.
On Windows, code signing certificates usually come in the
form of a .pfx
file. To use it with fbs, place
it at src/sign/windows/certificate.pfx
. Then,
set "windows_sign_pass"
to the password for
this file in either
src/build/settings/secret.json
,
.../windows.json
or .../base.json
.
Optionally, you can also set
"windows_sign_server"
to the timestamp server
that should be used for signing. For example:
"http://sha256timestamp.ws.symantec.com/sha256/timestamp"
.
Next you need to ensure you have Windows's
signtool
and that it is on your
PATH
. For instructions how to do this, please
see
here.
Once you have performed these steps, you can use the
commands fbs sign
and
fbs sign_installer
to code sign your app's
frozen binaries and its installer, respectively.
Once your app is installed on somebody else's computer, you will want to know when errors (/exceptions) occur running your app. With associated stack traces, this can be invaluable for learning about problems and fixing them.
fbs can upload errors that occur in your app to Sentry. This gives you a web interface for inspecting exceptions and stack traces.
To enable error tracking for your app, create a Sentry account and project. This gives you a DSN / Client Key similar to:
https://4e78a0...@sentry.io/12345
Save this as a setting to your
src/build/settings/base.json
. Also add the
setting's name to public_settings
. For example:
{ ..., "sentry_dsn": "https://4e78a0...@sentry.io/12345", "public_settings": ["sentry_dsn"] }
Next, install the necessary dependencies via:
pip install fbs[sentry]
(Don't forget to also add this dependency to your requirements/.)
Now you can add SentryExceptionHandler
to
the exception_handlers
property of your
ApplicationContext
:
from fbs_runtime import PUBLIC_SETTINGS from fbs_runtime.application_context import cached_property, \ is_frozen from fbs_runtime.application_context.PyQt6 import ApplicationContext from fbs_runtime.excepthook.sentry import SentryExceptionHandler class AppContext(ApplicationContext): ... @cached_property def exception_handlers(self): result = super().exception_handlers if is_frozen(): result.append(self.sentry_exception_handler) return result @cached_property def sentry_exception_handler(self): return SentryExceptionHandler( PUBLIC_SETTINGS['sentry_dsn'], PUBLIC_SETTINGS['version'], PUBLIC_SETTINGS['environment'] )
This only sends errors for the frozen (i.e. compiled) form
of your app. fbs automatically sets the
environment
setting to either
local
for when you're developing locally, or
production
for the
release version of your app. This
lets you distinguish the two in Sentry.
Often, you want extra information such as the user's
operating system when an exception occurs. You can set this
via the .scope
property of the Sentry exception
handler. It is only available once the exception handler was
initialized, so you need to use the callback
parameter:
@cached_property def sentry_exception_handler(self): return SentryExceptionHandler(..., callback=self._on_sentry_init) def _on_sentry_init(self): scope = self.sentry_exception_handler.scope from fbs_runtime import platform scope.set_extra('os', platform.name()) scope.user = {'id': 41, 'email': 'john@gmail.com'}
For more information about the additional data you can log this way, see Sentry's documentation.
Commercial desktop applications often require a license protection scheme. This prevents users who have not yet bought your app from using it. The typical vehicle for this are license keys: When a user purchases your app, you send them a license key that unlocks your application.
fbs makes it very easy for you to implement a reasonably secure license scheme. To do this, first install the necessary Python dependencies:
pip install fbs[licensing]
(Don't forget to also add this dependency to your requirements/.)
Then, use fbs's init_licensing
command to
generate a public/private key pair. This will be used for
creating and verifying your license keys:
fbs init_licensing
The workflow then is as follows:
When a user buys your app, generate a license key via the Python code
# This file was generated by `init_licensing` above: secret_json = 'src/build/settings/secret.json' import json privkey = json.load(open(secret_json))['licensing_privkey'] from fbs_runtime.licensing import pack_license_key print(pack_license_key({'email': 'user@domain.com'}, privkey))
Say the user saves the output of the above
print(...)
statement at
C:\license.key
. Then your application can
verify that the user is licensed with the following code:
from fbs_runtime import PUBLIC_SETTINGS from fbs_runtime.licensing import unpack_license_key def get_license_key(): with open(r'C:\license.key') as f: key_contents = f.read() return unpack_license_key(key_contents, PUBLIC_SETTINGS['licensing_pubkey'])
This raises FileNotFoundError
or
fbs_runtime.licensing.InvalidKey
if the user
does not have a valid license key. Otherwise, it returns the
key data {'email': ...}
.
For background information about fbs's implementation, see
this article.
At some point, you may want to define your own commands in
addition to the built-in ones run
,
freeze
etc. For example, you may want to create
a command that automatically uploads the produced binaries
to your web site.
fbs lets you define custom commands via the
@command
decorator. Create a file
build.py
next to your src/
directory, with the following contents:
from fbs.cmdline import command from os.path import dirname import fbs.cmdline @command def hi(): print('Hello World!') if __name__ == '__main__': project_dir = dirname(__file__) fbs.cmdline.main(project_dir)
Then, you can execute the following on the command line:
python build.py hi
But also, you can execute all of fbs's built-in commands. For instance:
python build.py run
As your build script grows more complex, it is recommended
that you split it into two parts: Put the command
definitions (the "what") into build.py
and
their implementation (the "how") into
src/build/python
. (If you use this approach,
you will also have to add src/build/python
to
sys.path
at the beginning of
build.py
.)
fbs consists of two Python packages: fbs
and
fbs_runtime
. The first implements the built-in
commands. The second contains logic for actually running
your app on your users' computers.
When you use fbs, you typically add references to
fbs_runtime
to your source code. For example,
the
default main.py
does this with ApplicationContext
from this
package.
On the other hand, your code does not necessarily have to
mention the fbs
package.
Some functions in this package however are exposed to let
you define custom commands,
or to modify the behaviour of fbs's built-in ones.
What you usually don't want to do is to refer to
fbs
from your application's implementation
(src/main/
). If at all, you should only refer to
fbs
from build scripts (build.py
and/or src/build/python/
).
This class is the main point of contact between fbs and your
application. As mentioned
above, fbs requires you
instantiate it. It lies in the module
fbs_runtime.application_context.PyQt6
(or
fbs_runtime.application_context.PySide6
if you
are using PySide6) and has the following methods and
properties:
This method returns the absolute path to the
resource file with the given
name or (relative) path. For example, if you have
src/main/resources/base/image.jpg
and call
get_resource('image.jpg')
, then this method
returns the absolute path to the image. If the given file
does not exist, a FileNotFoundError
is raised.
The implementation of this method transparently handles the
different subdirectories of src/main/resources
.
That is, if image.jpg
exists in both
src/main/resources/base
and
src/main/resources/mac
and you call it on Mac,
you obtain the absolute path to the latter.
This method also works both when you run your app from
source (via fbs run
), or when your users run
the compiled form of your app. In the first case, the path
in src/main/resources
is returned. In the
second, the path to the given file in your app's
installation directory. Do note that the files are only
filtered in the latter case.
This property holds the global QApplication object for your app. Every Qt GUI application has precisely one such object. fbs ensures that it is automatically instantiated.
You can use this property to access the QApplication object.
The canonical example of this is when you call
appctxt.app.exec()
, as required by fbs.
Another reason why this property is exposed by fbs is that
this lets you overwrite it. For instance, you may want to
use a custom subclass of QApplication
. Here is
how you might integrate it:
from fbs_runtime.application_context import cached_property from fbs_runtime.application_context.PyQt6 import ApplicationContext from PyQt6.QtWidgets import QApplication class MyAppContext(ApplicationContext): @cached_property def app(self): return MyCustomApp(sys.argv) ... class MyCustomApp(QApplication): ... if __name__ == '__main__': appctxt = MyAppContext() ... appctxt.app.exec()
For more information about @cached_property
,
see below.
This dict-like object exposes some build settings. A common use case is to display your app's version:
from fbs_runtime import PUBLIC_SETTINGS print('Starting version ' + PUBLIC_SETTINGS['version']) ...
The available settings are controlled by the setting
"public_settings"
. Eg., in the
default base.json
:
"public_settings": ["app_name", "author", "version"]
To extend this list, simply re-define it in one of your own
settings files, eg. base.json
. You don't have
to repeat the elements above because fbs automatically
concatenates lists defined in multiple settings files.
The motivation for only making some settings "public" is that settings often contain secret information such as passwords. We don't want these to be included in the frozen form of your app.
Every application developer needs to answer the following
question: How do I wire up the different objects that make
up my application? fbs's answer to this is an interplay of
the
ApplicationContext
class and @cached_property
.
fbs encourages (but does not force) you to instantiate all
components in your application context. For example: Say you
have a Window
class, which displays some
information from a Database
. Your application
context could look as follows:
from fbs_runtime.application_context import cached_property from fbs_runtime.application_context.PyQt6 import ApplicationContext class AppContext(ApplicationContext): @cached_property def window(self): return Window(self.db) @cached_property def db(self): return Database() def run(self): self.window.show() return self.app.exec() if __name__ == '__main__': appctxt = AppContext() appctxt.run()
When run()
is invoked when your application
starts, its first line accesses self.window
.
This executes the code in the definition of the
window(...)
property. This accesses
self.db
, which in turn executes the code in the
definition of db(...)
. The end result is that
we instantiate both Database
and
Window
, without a long stream of spaghetti
code.
Taking a step back, we see that the application context
becomes the "home" for all of your application's components.
Making it the one (and only) place where components are
instantiated makes it extremely easy to see how the
different parts of your application are connected. What's
more, @cached_property
ensures that each
component is only created once: Subsequent calls to the same
property return the result of the previous access.
Technically speaking, @cached_property
is
simply a Python
@property
that caches its results.
Having a central place / mechanism for wiring up components is a well-known technique called Dependency Injection. For further information, see for instance this article.
This module exposes several functions that let you determine the platform your app is executing on. Their names should be pretty self-explanatory:
is_windows()
is_mac()
is_linux()
is_ubuntu()
is_arch_linux()
is_fedora()
is_gnome_based()
is_kde_based()
Another function of potential interest is
name()
. It returns 'Windows'
,
'Mac'
or 'Linux'
, depending on the
current operating system.
The most important parts of fbs's API are described above.
But there are more functions which you can use. They are
documented in (extensive) comments in
fbs's source code. You can consider everything whose
name doesn't start with an underscore _
a part of
the public API.