How to integrate pretty-printing as part of build in bazel - c++

Right now, I have a really dumb pretty-print script which does a little git-fu to find files to format (unconditionally) and then runs those through clang-format -i. This approach has several shortcomings:
There are certain files which are enormous and take forever to pretty print.
The pretty printing is always done, regardless of whether or not the underlying file actually changed or not.
In the past, I was able to do things with CMake that had several nice properties which I would like to reproduce in bazel:
Only ever build code after it has gone through linting / pretty printing / etc.
Only lint / pretty print / etc. stuff that has changed
Pretty print stuff regardless of whether or not it is under VC or not
In CMake-land, I used this strategy, inspired by SCons proxy-target trickery:
Introduce a dummy target (e.g. source -> source.formatted). The action associated with this target does two things: a) run clang-format -i source, b) output/touch a file called source.formatted (this guarantees that for reasonable file systems, if source.formatted is newer than source, source doesn't need to be reformatted)
Add a dummy target (target_name.aggregated_formatted) which aggregates all the .formatted files corresponding to a particular library / executable target's sources
Make library / executable targets depend on target_name.aggregated_formatted as a pre-build step
Any help would be greatly appreciated.

#abergmeier is right. Let's take it one step further by implementing the macro and its components.
We'll use the C++ stage 1 tutorial in bazelbuild/examples.
Let's first mess up hello-world.cc:
#include <ctime>
#include <string>
#include <iostream>
std::string get_greet(const std::string& who) {
return "Hello " + who;
}
void print_localtime() {
std::time_t result =
std::time(nullptr);
std::cout << std::asctime(std::localtime(&result));
}
int main(int argc, char** argv) {
std::string who = "world";
if (argc > 1) {who = argv[1];}
std::cout << get_greet(who) << std::endl;
print_localtime();
return 0;
}
This is the BUILD file:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
Since cc_binary doesn't know anything about clang-format or linting in general, let's create a macro called clang_formatted_cc_binary and replace cc_binary with it. The BUILD file now looks like this:
load(":clang_format.bzl", "clang_formatted_cc_binary")
clang_formatted_cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
Next, create a file called clang_format.bzl with a macro named clang_formatted_cc_binary that's just a wrapper around native.cc_binary:
# In clang_format.bzl
def clang_formatted_cc_binary(**kwargs):
native.cc_binary(**kwargs)
At this point, you can build the cc_binary target, but it's not running clang-format yet. We'll need to add an intermediary rule to do that in clang_formatted_cc_binary which we'll call clang_format_srcs:
def clang_formatted_cc_binary(name, srcs, **kwargs):
# Using a filegroup for code cleaniness
native.filegroup(
name = name + "_unformatted_srcs",
srcs = srcs,
)
clang_format_srcs(
name = name + "_formatted_srcs",
srcs = [name + "_unformatted_srcs"],
)
native.cc_binary(
name = name,
srcs = [name + "_formatted_srcs"],
**kwargs
)
Note that we have replaced the native.cc_binary's sources with the formatted files, but kept the name to allow for in-place replacements of cc_binary -> clang_formatted_cc_binary in BUILD files.
Finally, we'll write the implementation of the clang_format_srcs rule, in the same clang_format.bzl file:
def _clang_format_srcs_impl(ctx):
formatted_files = []
for unformatted_file in ctx.files.srcs:
formatted_file = ctx.actions.declare_file("formatted_" + unformatted_file.basename)
formatted_files += [formatted_file]
ctx.actions.run_shell(
inputs = [unformatted_file],
outputs = [formatted_file],
progress_message = "Running clang-format on %s" % unformatted_file.short_path,
command = "clang-format %s > %s" % (unformatted_file.path, formatted_file.path),
)
return struct(files = depset(formatted_files))
clang_format_srcs = rule(
attrs = {
"srcs": attr.label_list(allow_files = True),
},
implementation = _clang_format_srcs_impl,
)
This rule goes through every file in the target's srcs attribute, declaring a "dummy" output file with the formatted_ prefix, and running clang-format on the unformatted file to produce the dummy output.
Now if you run bazel build :hello-world, Bazel will run the actions in clang_format_srcs before running the cc_binary compilation actions on the formatted files. We can prove this by running bazel build with the --subcommands flag:
$ bazel build //main:hello-world --subcommands
..
SUBCOMMAND: # //main:hello-world_formatted_srcs [action 'Running clang-format on main/hello-world.cc']
..
SUBCOMMAND: # //main:hello-world [action 'Compiling main/formatted_hello-world.cc']
..
SUBCOMMAND: # //main:hello-world [action 'Linking main/hello-world']
..
Looking at the contents of formatted_hello-world.cc, looks like clang-format did its job:
#include <ctime>
#include <string>
#include <iostream>
std::string get_greet(const std::string& who) { return "Hello " + who; }
void print_localtime() {
std::time_t result = std::time(nullptr);
std::cout << std::asctime(std::localtime(&result));
}
int main(int argc, char** argv) {
std::string who = "world";
if (argc > 1) {
who = argv[1];
}
std::cout << get_greet(who) << std::endl;
print_localtime();
return 0;
}
If all you want are the formatted sources without compiling them, you can run build the target with the _formatted_srcs suffix from clang_format_srcs directly:
$ bazel build //main:hello-world_formatted_srcs
INFO: Analysed target //main:hello-world_formatted_srcs (0 packages loaded).
INFO: Found 1 target...
Target //main:hello-world_formatted_srcs up-to-date:
bazel-bin/main/formatted_hello-world.cc
INFO: Elapsed time: 0.247s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action

You might be able to use aspects for that. Being not certain, a Bazel-dev will probably point that out if it indeed is possible.
If you are familiar with Rules and Actions and the like, the quick and dirty way (which is similar to the CMake hackery) is to write a Macro. For e.g. cc_library you would do:
def clean_cc_library(name, srcs, **kwargs):
lint_sources(
name = "%s_linted" % name,
srcs = srcs,
)
pretty_print_sources(
name = "%s_pretty" % name,
srcs = ["%s_linted"],
)
return native.cc_library(
name = name,
srcs = ["%s_pretty"],
**kwargs
)
Then you of course need to replace every cc_library with clean_cc_library. And lint_sources and pretty_print_sources are rules that you have to implement yourself and need to produce the list of cleaned up files.

#abergmeier mentions maybe being able to use Aspects. You can, and I've made a prototype of a general linting system that leverages Aspects functionality so that BUILD files do not need to be modified to use Macros like clang_formatted_cc_library in-place of the core rules.
The basic idea is to have a bazel build step that is a pure function f(linter, sources) -> linted_sources_diff and a subsequent bazel run step that takes those diffs and applies them back to your source code to fix lint errors.
The prototype implementation is available at https://github.com/thundergolfer/bazel-linting-system.

Related

CMake: How can I make add_custom_command output up-to-date by default?

I have a software environment based on CMake 3.11.4 and Python 3.7.
My libraries/programs have a config.txt file describing their dependencies in a format I specified. Then, I have a Python script (scripts/configure.py) that generates CMakeLists.txt on the fly and later calls CMake to generate a solution that can be built by Visual Studio 2015.
I want Python to be run again automatically when config.txt is edited by the user.
So I made my Python script add a custom command statement in the generated CMakeLists.txt. Here is how it looks like for a project named "myproject" including two libraries "lib1" and "lib2".
${SDE_ROOT_DIR}/build/myproject/CMakeLists.txt contains:
# Automatically re-run configure project when an input file changes:
set( PROJECT_DEPENDENCIES )
list( APPEND PROJECT_DEPENDENCIES "${SDE_ROOT_DIR}/lib/lib1/config.txt" )
list( APPEND PROJECT_DEPENDENCIES "${SDE_ROOT_DIR}/lib/lib2/config.txt" )
ADD_CUSTOM_COMMAND( OUTPUT ${SDE_ROOT_DIR}/build/myproject/CMakeLists.txt COMMAND tools/python/Python370/python.exe scripts/configure.py myproject WORKING_DIRECTORY ${SDE_ROOT_DIR} DEPENDS ${PROJECT_DEPENDENCIES} )
Here is what I do:
I run my script (scripts/configure.py myproject) to have the CMakeLists.txt and the Visual Studio solution generated.
Then I open the solution
The first time I build, it reports Generating CMakeLists.txt and I see that my script scripts/configure.py is invoked. This is not expected!
The second time I build, nothing happens. This is OK.
If I edit config.txt, the next time I build I see Generating CMakeLists.txt and I see that my script scripts/configure.py is invoked. That's good.
It's almost what I expected except the fact that my script is run the first time I compile the project. As CMakeLists.txt was just generated and is definitely newer than config.txt, I don't understand why it needs to generate CMakeLists.txt again.
Any idea what I could be doing wrong? Is there any additional command I should add to the CMakeLists.txt to make this custom command's output be "up to date" by default?
Here is a MCVE (config.txt is replaced by prgname.txt):
prg/main.cpp:
#include <iostream>
int main( int argc, char* argv[] )
{
std::cout << "Hello World!" << std::endl;
return 0;
}
prg/prgname.txt:
myprogram
scripts/configure.py:
import sys
import subprocess
import argparse
import os
from contextlib import contextmanager
#contextmanager
def pushd(newDir):
previousDir = os.getcwd()
os.chdir(newDir)
yield
os.chdir(previousDir)
def configure_project():
# check configuration args
parser = argparse.ArgumentParser(description="CMakeLists generator.")
parser.add_argument('project', metavar='project', type=str, help='project name')
args = parser.parse_args()
working_directory = os.getcwd()
project = args.project
buildfolder = os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), os.pardir, "build", project ))
if not os.path.isdir(buildfolder):
os.makedirs(buildfolder)
prgsourcefolder = os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), os.pardir, "prg" ))
prgbuildfolder = os.path.join( buildfolder, "prg" )
if not os.path.isdir(prgbuildfolder):
os.makedirs(prgbuildfolder)
prgnamepath = os.path.join( prgsourcefolder, "prgname.txt" )
with open( prgnamepath, "r" ) as prgnamefile:
prgname = prgnamefile.read()
with open( os.path.join( prgbuildfolder, "CMakeLists.txt" ), "w" ) as cmakelists:
cmakelists.write( "add_executable(" + prgname + " " + os.path.join(prgsourcefolder,"main.cpp").replace("\\","/") + ")\n" )
cmakelistspath = os.path.join( buildfolder, "CMakeLists.txt" )
with open( cmakelistspath, "w" ) as maincmakelists:
maincmakelists.write( "cmake_minimum_required(VERSION 3.11)\n" )
maincmakelists.write( "project(" + project + ")\n" )
maincmakelists.write( "add_subdirectory(prg)\n" )
maincmakelists.write( "add_custom_command( OUTPUT " + cmakelistspath.replace("\\","/") + " COMMAND python " + " ".join( [ x.replace("\\","/") for x in sys.argv] ) + " WORKING_DIRECTORY " + working_directory.replace("\\","/") + " DEPENDS " + prgnamepath.replace("\\","/") + ")\n" )
# Run CMake:
with pushd( buildfolder ):
cmd = ['cmake.exe', '-G', 'Visual Studio 14 2015 Win64', buildfolder]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
out = proc.stdout.read(1)
if proc.poll() != None:
break
sys.stdout.write(out.decode())
sys.stdout.flush()
proc.wait()
if __name__ == "__main__":
import sys
sys.exit( configure_project() )
Add CMake and Python to your PATH
From the scripts folder run python configure.py myproject
Open build/myproject/myproject.sln
Hit "compile all" and you'll see the unexpected message Generating CMakeLists.txt in the log
add_custom_command creates a command that no target depends on. Because of that, it won't be executed. I have no idea why Visual Studio runs it anyway the first time, but on Linux the Python script never runs.
For it to work properly, you also need a target. In this case, you can use add_custom_target with the ALL option and specify the output of the custom command as its dependency. This way, the custom target will run the command if needed.
Just add a line like
maincmakelists.write( "add_custom_target( Configure ALL DEPENDS " + cmakelistspath.replace("\\","/") + " )\n" )
after the one where you write add_custom_command and it should work. It did for me.
Note that the custom target will always run, but the custom command will only run when the prgname.txt file is modified.
Here is a "workaround", not a "solution".
I simply add a status file to tell when generate called from VS should be skipped because I know the solution is up-to-date:
Add new argument to the parser:
parser.add_argument('--from_vs', action='store_true', help='identify that configure is ran from VS to prevent useless regeneration, don't set this manually please')
Correctly maintain this file from the script itself:
vs_force_up_to_date_file = os.path.join( buildfolder, "vs_force_up_to_date" )
if args.from_vs:
is_up_to_date = False
if os.path.isfile( vs_force_up_to_date_file ):
with open( vs_force_up_to_date_file, "r" ) as file:
content = file.readlines()[0]
is_up_to_date = ( content == "True" )
if is_up_to_date:
# It's the first time VS runs this script, we know it is up-to-date, so let's not regenerate
# See https://stackoverflow.com/questions/59861101/cmake-how-can-i-make-add-custom-command-output-up-to-date-by-default
print( "First time calling generate from VS, project is likely up-to-date, let's not reconfigure!" )
# Make next generate from VS be skipped
with open( vs_force_up_to_date_file, "w" ) as file:
file.write( str(False) )
exit(0)
else:
# need to generate, let's continue!
pass
else:
# Generating from console, let's make VS believe it is up to date for first time it will generate
if os.path.isfile( vs_force_up_to_date_file ):
# Let the file as it is, "True" if VS generate never ran, else False
pass
else:
# Create the file to prevent first VS generate to rerun this script while it does not need to be ran
with open( vs_force_up_to_date_file, "w" ) as file:
file.write( str(True) )
Make this new argument be set when ran from VS:
run_args = " ".join( [ x.replace("\\","/") for x in sys.argv] )
if not args.from_vs:
run_args += " --from_vs"
maincmakelists.write( "add_custom_command( OUTPUT " + cmakelistspath.replace("\\","/") + " COMMAND python " + run_args + " WORKING_DIRECTORY " + working_directory.replace("\\","/") + " DEPENDS " + prgnamepath.replace("\\","/") + ")\n" )
This makes VS useless call to configure on first generate request be skipped and leter, needed calls will work as expected.
Edit
Actually, this does not work as well as expected. Because VS runs the script every first time for every configuration. So after you build once in Release mode, switching to Debug will end up generating again the CMakeLists.txt while it should not because first generation wrote False in vs_force_up_to_date file. This is a too naive solution.
Instead, the solution I finally adopted is to pass the path to all input files (prgname.txt) and output files (CMakeLists.txt) to the script and have it check if all outputs are more recent than all inputs, skip the generation. Then, whatever unexpected call to the script VS may do, they will be handled correctly by the script itself.

accessing recources when compiling QT application with Bazel

I'm using Bazel to compile a Qt application (https://github.com/bbreslauer/qt-bazel-example) that is using shaders defined in a qrc file.
When I'm trying to access the resource file, it is not available (as I did not connect the qrc file to the compilation).
How can I define the qrc file content in the build?
UPDATE
following the response by #ypnos, I'm trying to add a macro to my qt.bzl file. I would like the macro to recieve a list of files as an argument, create the (temporary) qrc file, and run the rcc command.
I am currently struggling with:
running a python script in the bzl file is not as straightforward as I though. It cannot generate a file ("open" is undefined). Is it possible? if yes how (see example below)
even with a given qrc file, I cant get the command to work, I guess i'm doing somthing wrong with the command line arguments but I cant find refrence/manual for that
this is what I got so far(my qt.bzl file)
...
def qt_resource(name,file_list, **kwargs):
## following doesnt work inside the bzl file:
# fid = open('%s.qrc' % name, 'w')
# fid.write("<RCC>\n")
# fid.write("\t<qresource prefix=\"/%s\">\n" % name)
# for x in file_list:
# fid.write("\t\t<file>%s</file>\n" % x)
# fid.write("\t</qresource>\n")
# fid.write("</RCC>\n")
# fid.close()
native.genrule(
name = "%s_res" % name,
outs = ["rcc_%s.cpp" % name],
cmd = "rcc %s.qrc -o $#/rcc_%s.cpp"%(name,name) ,
)
srcs = [":rcc_%s.cpp" % name]
native.cc_library(
name = name,
srcs = srcs,
hdrs = [],
deps = [],
**kwargs
)
It seems the bazel example that you are using does not come with support for qrc (it only does moc and ui files).1
QRC files need to be transformed into C++ sources using rcc and then compiled.2 The concept is similar to the one of .ui files which are converted to headers.
Maybe you can patch qt.bzl to add that functionality.

llvm: dyld: Symbol not found: __ZN4llvm11RuntimeDyld13MemoryManager6anchorEv

I am playing with LLVM and I hit an issue when trying to use JIT. I was able to build a compiler, it can be compiled, linked and it runs correctly (it compiles my toy programs). However, when I am trying to use build a JIT, it fails.
dyld: Symbol not found: __ZN4llvm11RuntimeDyld13MemoryManager6anchorEv
Referenced from: /Users/gruszczy/Projects/shwifty/./bazel-bin/_solib_darwin//liblibjit.so
Expected in: flat namespace
in /Users/gruszczy/Projects/shwifty/./bazel-bin/_solib_darwin//liblibjit.so
Abort trap: 6
I use Bazel to build everything, these are my build rules:
new_local_repository(
name = "llvm",
path = "/opt/local/libexec/llvm-4.0",
build_file= "llvm.BUILD")
cc_library(
name = "main",
srcs = glob(["lib/*.a"]),
hdrs = glob(["include/**/*.*"]),
visibility = ["//visibility:public"],
copts = ["-Iexternal/llvm/include"],
)
I use JIT in tests (I generate IR in the test then jit it, then run the method to see if it worked).
cc_library(
name = "jit",
srcs = ["jit.cc"],
hdrs = ["jit.h"],
deps = [
":ast",
":common",
"#llvm//:main"
],
copts = GENERAL_COPTS)
cc_test(
name = "codegen_test",
srcs = ["codegen_test.cc"],
deps = [
":ast",
":jit",
":lexer",
":parser",
":codegen",
"#gtest//:main",
"#llvm//:main"
],
copts = TEST_COPTS,
data = [":examples"],
size = "small"
)
Any suggestions what I might be missing?
The source of confusion is that Bazel by default links binaries statically, but tests dynamically. This makes the test-code-refactor loop faster, because changes to the test code only trigger the rebuild of the test, not the whole application. It can be disabled by setting linkstatic = 1 on codegen_test target.
As to why the symbols are not present in codegen_test when built as a shared library, that's much harder question and would need more project-specific information. But a possible solution might be to mark targets producing VMRuntimeDyld.a and VMMCJit.a as alwayslink = 1.
For the completeness, here's the link to an issue you reported on bazel.

Create version number variations for info.plist using #define and clang?

Years ago, when compiling with GCC, the following defines in a #include .h file could be pre-processed for use in info.plist:
#define MAJORVERSION 2
#define MINORVERSION 6
#define MAINTVERSION 4
<key>CFBundleShortVersionString</key> <string>MAJORVERSION.MINORVERSION.MAINTVERSION</string>
...which would turn into "2.6.4". That worked because GCC supported the "-traditional" flag. (see Tech Note TN2175 Info.plist files in Xcode Using the C Preprocessor, under "Eliminating whitespace between tokens in the macro expansion process")
However, fast-forward to 2016 and Clang 7.0.2 (Xcode 7.2.1) apparently does not support either "-traditional" or "-traditional-cpp" (or support it properly), yielding this string:
"2 . 6 . 4"
(see Bug 12035 - Preprocessor inserts spaces in macro expansions, comment 4)
Because there are so many different variations (CFBundleShortVersionString, CFBundleVersion, CFBundleGetInfoString), it would be nice to work around this clang problem, and define these once, and concatenate / stringify the pieces together. What is the commonly-accepted pattern for doing this now? (I'm presently building on MacOS but the same pattern would work for IOS)
Here is the Python script I use to increment my build number, whenever a source code change is detected, and update one or more Info.plist files within the project.
It was created to solve the issue raised in this question I asked a while back.
You need to create buildnum.ver file in the source tree that looks like this:
version 1.0
build 1
(you will need to manually increment version when certain project milestones are reached, but buildnum is incremented automatically).
NOTE the location of the .ver file must be in the root of the source tree (see SourceDir, below) as this script will look for modified files in this directory. If any are found, the build number is incremented. Modified means source files changes after the .ver file was last updated.
Then create a new Xcode target to run an external build tool and run something like:
tools/bump_buildnum.py SourceDir/buildnum.ver SourceDir/Info.plist
(make it run in ${PROJECT_DIR})
and then make all the actual Xcode targets dependent upon this target, so it runs before any of them are built.
#!/usr/bin/env python
#
# Bump build number in Info.plist files if a source file have changed.
#
# usage: bump_buildnum.py buildnum.ver Info.plist [ ... Info.plist ]
#
# andy#trojanfoe.com, 2014.
#
import sys, os, subprocess, re
def read_verfile(name):
version = None
build = None
verfile = open(name, "r")
for line in verfile:
match = re.match(r"^version\s+(\S+)", line)
if match:
version = match.group(1).rstrip()
match = re.match(r"^build\s+(\S+)", line)
if match:
build = int(match.group(1).rstrip())
verfile.close()
return (version, build)
def write_verfile(name, version, build):
verfile = open(name, "w")
verfile.write("version {0}\n".format(version))
verfile.write("build {0}\n".format(build))
verfile.close()
return True
def set_plist_version(plistname, version, build):
if not os.path.exists(plistname):
print("{0} does not exist".format(plistname))
return False
plistbuddy = '/usr/libexec/Plistbuddy'
if not os.path.exists(plistbuddy):
print("{0} does not exist".format(plistbuddy))
return False
cmdline = [plistbuddy,
"-c", "Set CFBundleShortVersionString {0}".format(version),
"-c", "Set CFBundleVersion {0}".format(build),
plistname]
if subprocess.call(cmdline) != 0:
print("Failed to update {0}".format(plistname))
return False
print("Updated {0} with v{1} ({2})".format(plistname, version, build))
return True
def should_bump(vername, dirname):
verstat = os.stat(vername)
allnames = []
for dirname, dirnames, filenames in os.walk(dirname):
for filename in filenames:
allnames.append(os.path.join(dirname, filename))
for filename in allnames:
filestat = os.stat(filename)
if filestat.st_mtime > verstat.st_mtime:
print("{0} is newer than {1}".format(filename, vername))
return True
return False
def upver(vername):
(version, build) = read_verfile(vername)
if version == None or build == None:
print("Failed to read version/build from {0}".format(vername))
return False
# Bump the version number if any files in the same directory as the version file
# have changed, including sub-directories.
srcdir = os.path.dirname(vername)
bump = should_bump(vername, srcdir)
if bump:
build += 1
print("Incremented to build {0}".format(build))
write_verfile(vername, version, build)
print("Written {0}".format(vername))
else:
print("Staying at build {0}".format(build))
return (version, build)
if __name__ == "__main__":
if os.environ.has_key('ACTION') and os.environ['ACTION'] == 'clean':
print("{0}: Not running while cleaning".format(sys.argv[0]))
sys.exit(0)
if len(sys.argv) < 3:
print("Usage: {0} buildnum.ver Info.plist [... Info.plist]".format(sys.argv[0]))
sys.exit(1)
vername = sys.argv[1]
(version, build) = upver(vername)
if version == None or build == None:
sys.exit(2)
for i in range(2, len(sys.argv)):
plistname = sys.argv[i]
set_plist_version(plistname, version, build)
sys.exit(0)
First, I would like to clarify what each key is meant to do:
CFBundleShortVersionString
A string describing the released version of an app, using semantic versioning. This string will be displayed in the App Store description.
CFBundleVersion
A string specifing the build version (released or unreleased). It is a string, but Apple recommends to use numbers instead.
CFBundleGetInfoString
Seems to be deprecated, as it is no longer listed in the Information Property List Key Reference.
During development, CFBundleShortVersionString isn't changed that often, and I normally set CFBundleShortVersionString manually in Xcode. The only string I change regularly is CFBundleVersion, because you can't submit a new build to iTunes Connect/TestFlight, if the CFBundleVersion wasn't changed.
To change the value, I use a Rake task with PlistBuddy to write a time stamp (year, month, day, hour, and minute) to CFBundleVersion:
desc "Bump bundle version"
task :bump_bundle_version do
bundle_version = Time.now.strftime "%Y%m%d%H%M"
sh %Q{/usr/libexec/PlistBuddy -c "Set CFBundleVersion #{bundle_version}" "DemoApp/DemoApp-Info.plist"}
end
You can use PlistBuddy, if you need to automate CFBundleShortVersionString as well.

Why does output of fltk-config truncate arguments to gcc?

I'm trying to build an application I've downloaded which uses the SCONS "make replacement" and the Fast Light Tool Kit Gui.
The SConstruct code to detect the presence of fltk is:
guienv = Environment(CPPFLAGS = '')
guiconf = Configure(guienv)
if not guiconf.CheckLibWithHeader('lo', 'lo/lo.h','c'):
print 'Did not find liblo for OSC, exiting!'
Exit(1)
if not guiconf.CheckLibWithHeader('fltk', 'FL/Fl.H','c++'):
print 'Did not find FLTK for the gui, exiting!'
Exit(1)
Unfortunately, on my (Gentoo Linux) system, and many others (Linux distributions) this can be quite troublesome if the package manager allows the simultaneous install of FLTK-1 and FLTK-2.
I have attempted to modify the SConstruct file to use fltk-config --cflags and fltk-config --ldflags (or fltk-config --libs might be better than ldflags) by adding them like so:
guienv.Append(CPPPATH = os.popen('fltk-config --cflags').read())
guienv.Append(LIBPATH = os.popen('fltk-config --ldflags').read())
But this causes the test for liblo to fail! Looking in config.log shows how it failed:
scons: Configure: Checking for C library lo...
gcc -o .sconf_temp/conftest_4.o -c "-I/usr/include/fltk-1.1 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_THREAD_SAFE -D_REENTRANT"
gcc: no input files
scons: Configure: no
How should this really be done?
And to complete my answer, how do I remove the quotes from the result of os.popen( 'command').read()?
EDIT The real question here is why does appending the output of fltk-config cause gcc to not receive the filename argument it is supposed to compile?
There are 2 similar ways to do this:
1)
conf = Configure(env)
status, _ = conf.TryAction("fltk-config --cflags")
if status:
env.ParseConfig("fltk-config --cflags")
else:
print "Failed fltk"
2)
try:
env.ParseConfig("fltk-config --cflags")
except (OSError):
print 'failed to run fltk-config you sure fltk is installed !?'
sys.exit(1)
This is quite a complex problem with no quick answer
I have referred to the instructions for using pkg-config with scons at http://www.scons.org/wiki/UsingPkgConfig. The following question is also helpful
Test if executable exists in Python?.
But we need to go a little bit further with these.
So after much investigation I discovered os.popen('command').read() does not trim the trailing newline '\n' which is what caused the truncation of the arguments sent to GCC.
We can use str.rstrip() to remove the trailing '\n'.
Secondly, as config.log shows, the arguments which fltk-config provides, SCONS wraps up in double quotes before giving them to GCC. I'm not exactly sure of the specifics but this is because the output of fltk-config (via os.popen) contains space characters.
We can use something like strarray = str.split(" ", str.count(" ")) to split the output into substrings where the space characters occur.
It is also worth noting that we were attempting to append the fltk-config --ldflags to the wrong variable within the GUI environment, they should have been added to LINKFLAGS.
Unfortunately this is only half way to the solution.
What we need to do is:
Find the full path of an executable on the system
Pass arguments to an executable and capture its output
Convert the output into a suitable format to append to the CPPFLAGS and LINKFLAGS.
So I have defined some functions to help...
1) Find full path of executable on system:
( see: Test if executable exists in Python? )
def ExecutablePath(program):
def is_exe(fpath):
return os.path.exists(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
1b) We also need to test for executable existence:
def CheckForExecutable(context, program):
context.Message( 'Checking for program %s...' %program )
if ExecutablePath(program):
context.Result('yes')
return program
context.Result('no')
2) Pass arguments to executable and place the output into an array:
def ExecutableOutputAsArray(program, args):
pth = ExecutablePath(program)
pargs = shlex.split('%s %s' %(pth, args))
progout = subprocess.Popen( pargs , stdout=subprocess.PIPE).communicate()[0]
flags = progout.rstrip()
return flags.split(' ', flags.count(" "))
Some usage:
guienv.Append(CPPFLAGS = ExecutableOutputAsArray('fltk-config', '--cflags') )
guienv.Append(LINKFLAGS = ExecutableOutputAsArray('fltk-config', '--ldflags') )
guienv.Append(LINKFLAGS = ExecutableOutputAsArray('pkg-config', '--libs liblo') )