1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
3 #include "cmFindProgramCommand.h"
4
5 #include <algorithm>
6 #include <string>
7 #include <utility>
8
9 #include "cmMakefile.h"
10 #include "cmMessageType.h"
11 #include "cmPolicies.h"
12 #include "cmStateTypes.h"
13 #include "cmStringAlgorithms.h"
14 #include "cmSystemTools.h"
15
16 class cmExecutionStatus;
17
18 #if defined(__APPLE__)
19 # include <CoreFoundation/CoreFoundation.h>
20 #endif
21
22 struct cmFindProgramHelper
23 {
cmFindProgramHelpercmFindProgramHelper24 cmFindProgramHelper(std::string debugName, cmMakefile* makefile,
25 cmFindBase const* base)
26 : DebugSearches(std::move(debugName), base)
27 , Makefile(makefile)
28 , PolicyCMP0109(makefile->GetPolicyStatus(cmPolicies::CMP0109))
29 {
30 #if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
31 // Consider platform-specific extensions.
32 this->Extensions.push_back(".com");
33 this->Extensions.push_back(".exe");
34 #endif
35 // Consider original name with no extensions.
36 this->Extensions.emplace_back();
37 }
38
39 // List of valid extensions.
40 std::vector<std::string> Extensions;
41
42 // Keep track of the best program file found so far.
43 std::string BestPath;
44
45 // Current names under consideration.
46 std::vector<std::string> Names;
47
48 // Current name with extension under consideration.
49 std::string TestNameExt;
50
51 // Current full path under consideration.
52 std::string TestPath;
53
54 // Debug state
55 cmFindBaseDebugState DebugSearches;
56 cmMakefile* Makefile;
57
58 cmPolicies::PolicyStatus PolicyCMP0109;
59
AddNamecmFindProgramHelper60 void AddName(std::string const& name) { this->Names.push_back(name); }
SetNamecmFindProgramHelper61 void SetName(std::string const& name)
62 {
63 this->Names.clear();
64 this->AddName(name);
65 }
CheckCompoundNamescmFindProgramHelper66 bool CheckCompoundNames()
67 {
68 return std::any_of(this->Names.begin(), this->Names.end(),
69 [this](std::string const& n) -> bool {
70 // Only perform search relative to current directory
71 // if the file name contains a directory separator.
72 return n.find('/') != std::string::npos &&
73 this->CheckDirectoryForName("", n);
74 });
75 }
CheckDirectorycmFindProgramHelper76 bool CheckDirectory(std::string const& path)
77 {
78 return std::any_of(this->Names.begin(), this->Names.end(),
79 [this, &path](std::string const& n) -> bool {
80 // Only perform search relative to current directory
81 // if the file name contains a directory separator.
82 return this->CheckDirectoryForName(path, n);
83 });
84 }
CheckDirectoryForNamecmFindProgramHelper85 bool CheckDirectoryForName(std::string const& path, std::string const& name)
86 {
87 return std::any_of(this->Extensions.begin(), this->Extensions.end(),
88 [this, &path, &name](std::string const& ext) -> bool {
89 if (!ext.empty() && cmHasSuffix(name, ext)) {
90 return false;
91 }
92 this->TestNameExt = cmStrCat(name, ext);
93 this->TestPath = cmSystemTools::CollapseFullPath(
94 this->TestNameExt, path);
95 bool exists = this->FileIsExecutable(this->TestPath);
96 exists ? this->DebugSearches.FoundAt(this->TestPath)
97 : this->DebugSearches.FailedAt(this->TestPath);
98 if (exists) {
99 this->BestPath = this->TestPath;
100 return true;
101 }
102 return false;
103 });
104 }
FileIsExecutablecmFindProgramHelper105 bool FileIsExecutable(std::string const& file) const
106 {
107 switch (this->PolicyCMP0109) {
108 case cmPolicies::OLD:
109 return cmSystemTools::FileExists(file, true);
110 case cmPolicies::NEW:
111 case cmPolicies::REQUIRED_ALWAYS:
112 case cmPolicies::REQUIRED_IF_USED:
113 return cmSystemTools::FileIsExecutable(file);
114 default:
115 break;
116 }
117 bool const isExeOld = cmSystemTools::FileExists(file, true);
118 bool const isExeNew = cmSystemTools::FileIsExecutable(file);
119 if (isExeNew == isExeOld) {
120 return isExeNew;
121 }
122 if (isExeNew) {
123 this->Makefile->IssueMessage(
124 MessageType::AUTHOR_WARNING,
125 cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0109),
126 "\n"
127 "The file\n"
128 " ",
129 file,
130 "\n"
131 "is executable but not readable. "
132 "CMake is ignoring it for compatibility."));
133 } else {
134 this->Makefile->IssueMessage(
135 MessageType::AUTHOR_WARNING,
136 cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0109),
137 "\n"
138 "The file\n"
139 " ",
140 file,
141 "\n"
142 "is readable but not executable. "
143 "CMake is using it for compatibility."));
144 }
145 return isExeOld;
146 }
147 };
148
cmFindProgramCommand(cmExecutionStatus & status)149 cmFindProgramCommand::cmFindProgramCommand(cmExecutionStatus& status)
150 : cmFindBase("find_program", status)
151 {
152 this->NamesPerDirAllowed = true;
153 this->VariableDocumentation = "Path to a program.";
154 this->VariableType = cmStateEnums::FILEPATH;
155 }
156
157 // cmFindProgramCommand
InitialPass(std::vector<std::string> const & argsIn)158 bool cmFindProgramCommand::InitialPass(std::vector<std::string> const& argsIn)
159 {
160 this->DebugMode = this->ComputeIfDebugModeWanted();
161 this->CMakePathName = "PROGRAM";
162
163 // call cmFindBase::ParseArguments
164 if (!this->ParseArguments(argsIn)) {
165 return false;
166 }
167
168 if (this->AlreadyDefined) {
169 this->NormalizeFindResult();
170 return true;
171 }
172
173 std::string const result = this->FindProgram();
174 this->StoreFindResult(result);
175 return true;
176 }
177
FindProgram()178 std::string cmFindProgramCommand::FindProgram()
179 {
180 std::string program;
181
182 if (this->SearchAppBundleFirst || this->SearchAppBundleOnly) {
183 program = this->FindAppBundle();
184 }
185 if (program.empty() && !this->SearchAppBundleOnly) {
186 program = this->FindNormalProgram();
187 }
188
189 if (program.empty() && this->SearchAppBundleLast) {
190 program = this->FindAppBundle();
191 }
192 return program;
193 }
194
FindNormalProgram()195 std::string cmFindProgramCommand::FindNormalProgram()
196 {
197 if (this->NamesPerDir) {
198 return this->FindNormalProgramNamesPerDir();
199 }
200 return this->FindNormalProgramDirsPerName();
201 }
202
FindNormalProgramNamesPerDir()203 std::string cmFindProgramCommand::FindNormalProgramNamesPerDir()
204 {
205 // Search for all names in each directory.
206 cmFindProgramHelper helper(this->FindCommandName, this->Makefile, this);
207 for (std::string const& n : this->Names) {
208 helper.AddName(n);
209 }
210
211 // Check for the names themselves if they contain a directory separator.
212 if (helper.CheckCompoundNames()) {
213 return helper.BestPath;
214 }
215
216 // Search every directory.
217 for (std::string const& sp : this->SearchPaths) {
218 if (helper.CheckDirectory(sp)) {
219 return helper.BestPath;
220 }
221 }
222 // Couldn't find the program.
223 return "";
224 }
225
FindNormalProgramDirsPerName()226 std::string cmFindProgramCommand::FindNormalProgramDirsPerName()
227 {
228 // Search the entire path for each name.
229 cmFindProgramHelper helper(this->FindCommandName, this->Makefile, this);
230 for (std::string const& n : this->Names) {
231 // Switch to searching for this name.
232 helper.SetName(n);
233
234 // Check for the names themselves if they contain a directory separator.
235 if (helper.CheckCompoundNames()) {
236 return helper.BestPath;
237 }
238
239 // Search every directory.
240 for (std::string const& sp : this->SearchPaths) {
241 if (helper.CheckDirectory(sp)) {
242 return helper.BestPath;
243 }
244 }
245 }
246 // Couldn't find the program.
247 return "";
248 }
249
FindAppBundle()250 std::string cmFindProgramCommand::FindAppBundle()
251 {
252 for (std::string const& name : this->Names) {
253
254 std::string appName = name + std::string(".app");
255 std::string appPath =
256 cmSystemTools::FindDirectory(appName, this->SearchPaths, true);
257
258 if (!appPath.empty()) {
259 std::string executable = this->GetBundleExecutable(appPath);
260 if (!executable.empty()) {
261 return cmSystemTools::CollapseFullPath(executable);
262 }
263 }
264 }
265
266 // Couldn't find app bundle
267 return "";
268 }
269
GetBundleExecutable(std::string const & bundlePath)270 std::string cmFindProgramCommand::GetBundleExecutable(
271 std::string const& bundlePath)
272 {
273 std::string executable;
274 (void)bundlePath;
275 #if defined(__APPLE__)
276 // Started with an example on developer.apple.com about finding bundles
277 // and modified from that.
278
279 // Get a CFString of the app bundle path
280 // XXX - Is it safe to assume everything is in UTF8?
281 CFStringRef bundlePathCFS = CFStringCreateWithCString(
282 kCFAllocatorDefault, bundlePath.c_str(), kCFStringEncodingUTF8);
283
284 // Make a CFURLRef from the CFString representation of the
285 // bundle’s path.
286 CFURLRef bundleURL = CFURLCreateWithFileSystemPath(
287 kCFAllocatorDefault, bundlePathCFS, kCFURLPOSIXPathStyle, true);
288
289 // Make a bundle instance using the URLRef.
290 CFBundleRef appBundle = CFBundleCreate(kCFAllocatorDefault, bundleURL);
291
292 // returned executableURL is relative to <appbundle>/Contents/MacOS/
293 CFURLRef executableURL = CFBundleCopyExecutableURL(appBundle);
294
295 if (executableURL != nullptr) {
296 const int MAX_OSX_PATH_SIZE = 1024;
297 UInt8 buffer[MAX_OSX_PATH_SIZE];
298
299 if (CFURLGetFileSystemRepresentation(executableURL, false, buffer,
300 MAX_OSX_PATH_SIZE)) {
301 executable = bundlePath + "/Contents/MacOS/" +
302 std::string(reinterpret_cast<char*>(buffer));
303 }
304 // Only release CFURLRef if it's not null
305 CFRelease(executableURL);
306 }
307
308 // Any CF objects returned from functions with "create" or
309 // "copy" in their names must be released by us!
310 CFRelease(bundlePathCFS);
311 CFRelease(bundleURL);
312 CFRelease(appBundle);
313 #endif
314
315 return executable;
316 }
317
cmFindProgram(std::vector<std::string> const & args,cmExecutionStatus & status)318 bool cmFindProgram(std::vector<std::string> const& args,
319 cmExecutionStatus& status)
320 {
321 return cmFindProgramCommand(status).InitialPass(args);
322 }
323