(Update with working solution at bottom)
From what I can tell, generating a dependency graph with custom targets is supported as of CMake 3.17. However, when attempting to do so, I get no dependency mapping between my targets and I get this warning:
CMake Warning:
Manually-specified variables were not used by the project:
GRAPHVIZ_CUSTOM_TARGETS
I stumbled upon this SO answer, but it hasn't helped much. The next-closest thing I could find was an incomplete Merge Request to support some different use cases.
I'm running this on Windows, but I don't know if that could be the issue.
So, I'm stumped and could use some help before I file an issue against KitWare... since I think I am missing something really obvious. Any help would be so appreciated.
CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(
gviz
LANGUAGES NONE
)
add_custom_command(
OUTPUT foo.txt
COMMAND cmake --version > foo.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_command(
OUTPUT bar.txt
COMMAND cmake --version > bar.txt
DEPENDS foo.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_target(
foo
DEPENDS foo.txt
)
add_custom_target(
bar
DEPENDS bar.txt
)
Command ran from console, and output:
Console Output:
cmake_graphviz> cmake --graphviz=group.dot -BNBuild -GNinja . -DGRAPHVIZ_CUSTOM_TARGETS=TRUE
-- Configuring done
-- Generating done
Generate graphviz: cmake_graphviz/group.dot
CMake Warning:
Manually-specified variables were not used by the project:
GRAPHVIZ_CUSTOM_TARGETS
-- Build files have been written to: cmake_graphviz/NBuild
And finally, the group.dot file:
digraph "gviz" {
node [
fontsize = "12"
];
subgraph clusterLegend {
label = "Legend";
color = black;
edge [ style = invis ];
legendNode0 [ label = "Executable", shape = egg ];
legendNode1 [ label = "Static Library", shape = octagon ];
legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
legendNode3 [ label = "Module Library", shape = tripleoctagon ];
legendNode4 [ label = "Interface Library", shape = pentagon ];
legendNode5 [ label = "Object Library", shape = hexagon ];
legendNode6 [ label = "Unknown Library", shape = septagon ];
legendNode7 [ label = "Custom Target", shape = box ];
legendNode0 -> legendNode1 [ style = solid ];
legendNode0 -> legendNode2 [ style = solid ];
legendNode0 -> legendNode3;
legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
legendNode3 -> legendNode6 [ style = solid ];
legendNode0 -> legendNode7;
}
}
Working solution:
CMakeLists.txt:
cmake_minimum_required(VERSION 3.18)
project(
gviz
LANGUAGES NONE
)
# Because I don't like cmake files flooding my root directory
file(COPY cmake/CMakeGraphVizOptions.cmake DESTINATION ${CMAKE_BINARY_DIR})
add_custom_command(
OUTPUT foo.txt
COMMAND cmake --version > foo.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_command(
OUTPUT bar.txt
COMMAND cmake --version > bar.txt
DEPENDS foo.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_target(
foo
DEPENDS foo.txt
)
add_custom_target(bar)
add_dependencies(bar foo)
cmake/CMakeGraphVizOptions.cmake
# This file sets some options to control GraphViz graphs.
set(GRAPHVIZ_GRAPH_NAME "MyGraph")
set(GRAPHVIZ_CUSTOM_TARGETS TRUE)
set(GRAPHVIZ_NODE_PREFIX "blah")
File List output:
group.dot
group.dot.bar
group.dot.foo
group.dot.bar.dependers
group.dot.bar.dependers
Now, the only issue I have is it doesn't seem to link dependencies via file outputs, but that is outside of the scope of this question :)
In the linked documentation, it states:
The look and content of the generated graphs can be controlled using the file CMakeGraphVizOptions.cmake. This file is first searched in CMAKE_BINARY_DIR, and then in CMAKE_SOURCE_DIR. If found, the variables set in it are used to adjust options for the generated Graphviz files.
So, instead of setting GRAPHVIZ_CUSTOM_TARGETS in the cmake command line, set it in a file called CMakeGraphVizOptions.cmake, and add this file to your top-level source directory.
An example CMakeGraphVizOptions.cmake:
# This file sets some options to control GraphViz graphs.
set(GRAPHVIZ_GRAPH_NAME "MyGraph")
set(GRAPHVIZ_CUSTOM_TARGETS TRUE)
Related
I have files with identical names but in different folders. Nextflow stages these files into the same work directory resulting in name collisions. My question is how to deal with that without renaming the files. Example:
# Example data
mkdir folder1 folder2
echo 1 > folder1/file.txt
echo 2 > folder2/file.txt
# We read from samplesheet
$ cat samplesheet.csv
sample,file
sample1,/home/atpoint/foo/folder1/file.txt
sample1,/home/atpoint/foo/folder2/file.txt
# Nextflow main.nf
#! /usr/bin/env nextflow
nextflow.enable.dsl=2
// Read samplesheet and group files by sample (first column)
samplesheet = Channel
.fromPath(params.samplesheet)
.splitCsv(header:true)
.map {
sample = it['sample']
file = it['file']
tuple(sample, file)
}
ch_samplesheet = samplesheet.groupTuple(by:0)
// That creates a tuple like:
// [sample1, [/home/atpoint/foo/folder1/file.txt, /home/atpoint/foo/folder2/file.txt]]
// Dummy process that stages both files into the same work directory folder
process PRO {
input:
tuple val(samplename), path(files)
output:
path("out.txt")
script:
"""
echo $samplename with files $files > out.txt
"""
}
workflow { PRO(ch_samplesheet) }
# Run it
NXF_VER=21.10.6 nextflow run main.nf --samplesheet $(realpath samplesheet.csv)
...obviously resulting in:
N E X T F L O W ~ version 21.10.6
Launching `main.nf` [adoring_jennings] - revision: 87f26fa90b
[- ] process > PRO -
Error executing process > 'PRO (1)'
Caused by:
Process `PRO` input file name collision -- There are multiple input files for each of the following file names: file.txt
So, what now? The real world application here is sequencing replicates of the same fastq file, which then have the same name, but are in different folders, and I want to feed them into a process that merges them. I am aware of this section in the docs but cannot say that any of it was helpful or that I understand it properly.
You can use stageAs option in your process definition.
#! /usr/bin/env nextflow
nextflow.enable.dsl=2
samplesheet = Channel
.fromPath(params.samplesheet)
.splitCsv(header:true)
.map {
sample = it['sample']
file = it['file']
tuple(sample, file)
}
.groupTuple()
.set { ch_samplesheet }
// [sample1, [/path/to/folder1/file.txt, /path/to/folder2/file.txt]]
process PRO {
input:
tuple val(samplename), path(files, stageAs: "?/*")
output:
path("out.txt")
shell:
def input_str = files instanceof List ? files.join(" ") : files
"""
cat ${input_str} > out.txt
"""
}
workflow { PRO(ch_samplesheet) }
See an example from nf-core and the path input type docs
Thanks to the comments, better understand the problem a bit. The variables:
thufir#dur:~/tcl/packages$
thufir#dur:~/tcl/packages$ echo 'puts $auto_path' | tclsh
/usr/share/tcltk/tcl8.6 /usr/share/tcltk /usr/lib /usr/local/lib/tcltk /usr/local/share/tcltk /usr/lib/tcltk/x86_64-linux-gnu /usr/lib/tcltk /usr/lib/tcltk/tcl8.6
thufir#dur:~/tcl/packages$
thufir#dur:~/tcl/packages$ echo 'puts $tcl_pkgPath' | tclsh
/usr/local/lib/tcltk /usr/local/share/tcltk /usr/lib/tcltk/x86_64-linux-gnu /usr/lib/tcltk /usr/share/tcltk /usr/lib/tcltk/tcl8.6 /usr/lib
thufir#dur:~/tcl/packages$
code:
thufir#dur:~/tcl/packages$
thufir#dur:~/tcl/packages$ ll
total 16
drwxrwxr-x 2 thufir thufir 4096 May 4 02:22 ./
drwxrwxr-x 6 thufir thufir 4096 May 4 02:22 ../
-rw-rw-r-- 1 thufir thufir 215 May 4 02:21 foo.tcl
-rw-rw-r-- 1 thufir thufir 1207 May 4 02:20 tutstack.tcl
thufir#dur:~/tcl/packages$
thufir#dur:~/tcl/packages$ cat foo.tcl
package require tutstack 1.0
set stack [tutstack::create]
foreach num {1 2 3 4 5} { tutstack::push $stack $num }
while { ![tutstack::empty $stack] } {
puts "[tutstack::pop $stack]"
}
tutstack::destroy $stack
thufir#dur:~/tcl/packages$
thufir#dur:~/tcl/packages$ cat tutstack.tcl
# Register the package
package provide tutstack 1.0
package require Tcl 8.5
# Create the namespace
namespace eval ::tutstack {
# Export commands
namespace export create destroy push pop peek empty
# Set up state
variable stack
variable id 0
}
# Create a new stack
proc ::tutstack::create {} {
variable stack
variable id
set token "stack[incr id]"
set stack($token) [list]
return $token
}
# Destroy a stack
proc ::tutstack::destroy {token} {
variable stack
unset stack($token)
}
# Push an element onto a stack
proc ::tutstack::push {token elem} {
variable stack
lappend stack($token) $elem
}
# Check if stack is empty
proc ::tutstack::empty {token} {
variable stack
set num [llength $stack($token)]
return [expr {$num == 0}]
}
# See what is on top of the stack without removing it
proc ::tutstack::peek {token} {
variable stack
if {[empty $token]} {
error "stack empty"
}
return [lindex $stack($token) end]
}
# Remove an element from the top of the stack
proc ::tutstack::pop {token} {
variable stack
set ret [peek $token]
set stack($token) [lrange $stack($token) 0 end-1]
return $ret
}
thufir#dur:~/tcl/packages$
thufir#dur:~/tcl/packages$ tclsh foo.tcl
can't find package tutstack 1.0
while executing
"package require tutstack 1.0"
(file "foo.tcl" line 1)
thufir#dur:~/tcl/packages$
to my understanding, I need to compile a list or map of where packages are.
The problem is that Tcl is not finding the index file (which should be called pkgIndex.tcl) for your package. If you had implemented the weather 1.0 package as a file weather.tcl, then you'd probably be looking to have an index file something like this in the same directory:
package ifneeded weather 1.0 [list source [file join $dir weather.tcl]]
That says “to load version 1.0 of the weather package, run this script” where the script is generated at runtime and binds $dir in (which is a variable always defined in the context where package index loader runs package ifneeded).
Once that's there, you need to allow Tcl to find the index file. This can be done by putting that directory or its immediate parent on the Tcl global auto_path list; either do that inside your script before you load any packages (very useful for applications that have internal packages) or you can initialise that from outside of Tcl too by setting the TCLLIBPATH environment variable. Note that the value of that variable is a Tcl list of directories, not a system path like env(PATH). This matters if you have backslashes or spaces in directory names, or if you want to have multiple elements on the list. Fortunately, you can usually avoid all of these issues in the case of adding a single directory as an environment variable, even on Windows, by using / instead of \ and by following usual installation practice and not putting a space in names. When adding a path during application launch it's easier: you just use lappend, perhaps like this (very early in your main script):
lappend auto_path [file join [file dirname [info script]] my_app_pacakges]
# If the script is in foo/bar.tcl then packages are in or below foo/my_app_packages
result which runs:
thufir#dur:~/tcl/foo$
thufir#dur:~/tcl/foo$ tree
.
├── api
│ ├── pkgIndex.tcl
│ └── tutstack.tcl
└── main.tcl
1 directory, 3 files
thufir#dur:~/tcl/foo$
thufir#dur:~/tcl/foo$ cat main.tcl
lappend auto_path /home/thufir/tcl/foo/api
package require tutstack 1.0
set stack [tutstack::create]
foreach num {1 2 3 4 5} { tutstack::push $stack $num }
while { ![tutstack::empty $stack] } {
puts "[tutstack::pop $stack]"
}
tutstack::destroy $stack
thufir#dur:~/tcl/foo$
thufir#dur:~/tcl/foo$ cat api/pkgIndex.tcl
# Tcl package index file, version 1.1
# This file is generated by the "pkg_mkIndex" command
# and sourced either when an application starts up or
# by a "package unknown" script. It invokes the
# "package ifneeded" command to set up package-related
# information so that packages will be loaded automatically
# in response to "package require" commands. When this
# script is sourced, the variable $dir must contain the
# full path name of this file's directory.
package ifneeded tutstack 1.0 [list source [file join $dir tutstack.tcl]]
thufir#dur:~/tcl/foo$
thufir#dur:~/tcl/foo$ cat api/tutstack.tcl
# Register the package
package provide tutstack 1.0
package require Tcl 8.5
# Create the namespace
namespace eval ::tutstack {
# Export commands
namespace export create destroy push pop peek empty
# Set up state
variable stack
variable id 0
}
# Create a new stack
proc ::tutstack::create {} {
variable stack
variable id
set token "stack[incr id]"
set stack($token) [list]
return $token
}
# Destroy a stack
proc ::tutstack::destroy {token} {
variable stack
unset stack($token)
}
# Push an element onto a stack
proc ::tutstack::push {token elem} {
variable stack
lappend stack($token) $elem
}
# Check if stack is empty
proc ::tutstack::empty {token} {
variable stack
set num [llength $stack($token)]
return [expr {$num == 0}]
}
# See what is on top of the stack without removing it
proc ::tutstack::peek {token} {
variable stack
if {[empty $token]} {
error "stack empty"
}
return [lindex $stack($token) end]
}
# Remove an element from the top of the stack
proc ::tutstack::pop {token} {
variable stack
set ret [peek $token]
set stack($token) [lrange $stack($token) 0 end-1]
return $ret
}
thufir#dur:~/tcl/foo$
thufir#dur:~/tcl/foo$ tclsh main.tcl
5
4
3
2
1
thufir#dur:~/tcl/foo$
generating the config file:
thufir#dur:~/tcl/foo/api$
thufir#dur:~/tcl/foo/api$ tclsh
%
%
% pkg_mkIndex . *.tcl
%
% exit
thufir#dur:~/tcl/foo/api$
I use CMake 3.10. Previously I've used 3.5.
According manual i can use $ to get some path to output file. In fact anything isn't printed.
Status is "-- lib_location == $" I looked into examples of
But if I use next construction, it works well.
add_custom_target(
testTartgetFile ALL
COMMAND ${CMAKE_COMMAND} -E echo "$<TARGET_FILE:tgt1>"
VERBATIM
)
The question is how to get target object? I need it for further handling, not for print out.
My code:
cmake_minimum_required(VERSION 3.0)
project(libtest_project)
function(add_txbundle)
set(options NONE)
set(oneValueArgs TARGET)
set(multiValueArgs EXTRA_MAPPINGS DEPENDENCIES)
set(txPrefix "TxBundle")
cmake_parse_arguments(${txPrefix} "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
message(status " TARGET ${${txPrefix}_TARGET}")
message(status " EXTRA_MAPPINS ${TxBundle_EXTRA_MAPPINGS}")
set(TxBundleTarget "${${txPrefix}_TARGET}.txbundle")
set(TxParentTarget "${${txPrefix}_TARGET}")
message(status " TX TARGET ${TxBundleTarget}")
#..... some actions ...
endfunction(add_txbundle)
add_library(testlb SHARED testlib.cpp)
message (STATUS "lib_location == $<TARGET_FILE:testlb>")
add_txbundle(TARGET testlb EXTRA_MAPPINGS "1:1")
I want to execute a CMake command in a subdirectory with execute_process, and also pass some cache variables as -D options.
If the variable is of type string, it works. However, if the variable is a list, the typical method of passing a list in command line does not seem to work.
I tried all of the combinations listed in that answer. I even tried to join mylist with "\\;" or "\\\\;". However, the execute_process seems to always unpack the '-DVal2=a\\;b\\;c\\;' or '-DVal2=a;b;c' to -Dval2=a b c.
How can I prevent this? Only -DVal2=a\\;b\\;c works, but it's very annoying.
set(
mylist
a
b
c
)
set(
cmake_args
"-DVal1=abc"
"'-DVal2=${mylist}'" #does not work, the execute_process will unpack it into seperated args
)
execute_process(
COMMAND ${CMAKE_COMMAND} ${cmake_args} ${CMAKE_SOURCE_DIR}/subproject
OUTPUT_FILE ${CMAKE_BINARY_DIR}/config.log
ERROR_FILE ${CMAKE_BINARY_DIR}/config.log
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/subproject
RESULT_VARIABLE config_result
)
Before passing the list, run this line on it:
string(REPLACE ";" "\\;" escaped_list "${my_list}")
and then pass escaped_list. On the other end, it will have
the exact same value as my_list.
For example,
set(my_list "a\;b" "c" "d")
string(REPLACE ";" "\\;" escaped_list "${my_list}")
execute_process(COMMAND ${CMAKE_COMMAND} -Dmy_list=${escaped_list} -P test.cmake)
(Tested with cmake 3.17).
This also works when assigning first to cmake_args and passing that.
For example,
test1.cmake
# Construction.
set(my_list "a\;b" "c" "d")
set(other_list "e" "f\;g" "h")
# For debugging purposes.
message("my_list = \"${my_list}\".")
foreach(arg ${my_list})
message("-> ${arg}")
endforeach()
message("other_list = \"${other_list}\".")
foreach(arg ${other_list})
message("-> ${arg}")
endforeach()
# Encoding.
string(REPLACE ";" "\\;" escaped_list "${my_list}")
message("escaped_list = \"${escaped_list}\".")
string(REPLACE ";" "\\;" other_escaped_list "${other_list}")
message("other_escaped_list = \"${other_escaped_list}\".")
set(cmake_args "-Dother_list=${other_escaped_list}" "-Dmy_list=${escaped_list}")
execute_process(
COMMAND
${CMAKE_COMMAND} ${cmake_args} -P test2.cmake
)
test2.cmake
# For debugging purpose.
message("my_list = \"${my_list}\".")
foreach(arg ${my_list})
message("-> ${arg}")
endforeach()
message("other_list = \"${other_list}\".")
foreach(arg ${other_list})
message("-> ${arg}")
endforeach()
Output of running cmake -P test1.cmake :
my_list = "a\;b;c;d".
-> a;b
-> c
-> d
other_list = "e;f\;g;h".
-> e
-> f;g
-> h
escaped_list = "a\\;b\;c\;d".
other_escaped_list = "e\;f\\;g\;h".
my_list = "a\;b;c;d".
-> a;b
-> c
-> d
other_list = "e;f\;g;h".
-> e
-> f;g
-> h
Please observe closely where double quotes were and weren't used.
I think you need to escape the ; character which is the default separator for lists in CMake, but it's not clear how you do it so that it doesn't work for you.
So, try something like this
set(mylist_str "")
foreach(item ${mylist})
string(APPEND mylist_str ${item} "\;")
endforeach()
# this is for debugging
message(STATUS "List as string: ${mylist_str}")
set(cmake_args
"-DVal1=abc"
"-DVal2=${mylist_str}"
"-DVal3=\"${mylist_str}\"" # this has quotes around it
)
# this is for debugging
foreach(item ${cmake_args})
message(STATUS "A list item: ${item}")
endforeach()
With CMake, how can I get a list of all the source files which go into an executable target, including all sources in all targets this executable depends on?
We have a pattern in the code base where initializer callers are generated by the build system based on file names and paths in the source tree. So I need the full path (or relative to source root) to all source files an executable target depends on.
Here is my piece of code to get one target's link dependencies:
function(target_link_libraries _target)
set(_mode "PUBLIC")
foreach(_arg IN LISTS ARGN)
if (_arg MATCHES "INTERFACE|PUBLIC|PRIVATE|LINK_PRIVATE|LINK_PUBLIC|LINK_INTERFACE_LIBRARIES")
set(_mode "${_arg}")
else()
if (NOT _arg MATCHES "debug|optimized|general")
set_property(GLOBAL APPEND PROPERTY GlobalTargetDepends${_target} ${_arg})
endif()
endif()
endforeach()
_target_link_libraries(${_target} ${ARGN})
endfunction()
function(get_link_dependencies _target _listvar)
set(_worklist ${${_listvar}})
if (TARGET ${_target})
list(APPEND _worklist ${_target})
get_property(_dependencies GLOBAL PROPERTY GlobalTargetDepends${_target})
foreach(_dependency IN LISTS _dependencies)
if (NOT _dependency IN_LIST _worklist)
get_link_dependencies(${_dependency} _worklist)
endif()
endforeach()
set(${_listvar} "${_worklist}" PARENT_SCOPE)
endif()
endfunction()
For older CMake versions (prior to 3.4), you will need to replace the IN_LIST check with a list(FIND ...) call:
[...]
list(FIND _worklist ${_dependency} _idx)
if (${_idx} EQUAL -1)
get_link_dependencies(${_dependency} _worklist)
endif()
[...]
And here is the test code I've used:
cmake_minimum_required(VERSION 3.4)
project(GetSources)
cmake_policy(SET CMP0057 NEW)
[... include functions posted above ...]
file(WRITE a.cc "")
add_library(A STATIC a.cc)
file(WRITE b.cc "")
add_library(B STATIC b.cc)
file(WRITE main.cc "int main() { return 0; }")
add_executable(${PROJECT_NAME} main.cc)
target_link_libraries(B A)
target_link_libraries(${PROJECT_NAME} B)
get_link_dependencies(${PROJECT_NAME} _deps)
foreach(_dep IN LISTS _deps)
get_target_property(_srcs ${_dep} SOURCES)
get_target_property(_src_dir ${_dep} SOURCE_DIR)
foreach(_src IN LISTS _srcs)
message("${_src_dir}/${_src}")
endforeach()
endforeach()
References
Recursive list of LINK_LIBRARIES in CMake