Serenity Operating System
1/*
2 * Copyright (c) 2022, Itamar S. <itamar8910@gmail.com>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "ProjectBuilder.h"
8#include <AK/LexicalPath.h>
9#include <LibCore/Command.h>
10#include <LibCore/DeprecatedFile.h>
11#include <LibRegex/Regex.h>
12#include <fcntl.h>
13#include <sys/stat.h>
14
15namespace HackStudio {
16
17ProjectBuilder::ProjectBuilder(NonnullRefPtr<TerminalWrapper> terminal, Project const& project)
18 : m_project_root(project.root_path())
19 , m_project(project)
20 , m_terminal(move(terminal))
21 , m_is_serenity(project.project_is_serenity() ? IsSerenityRepo::Yes : IsSerenityRepo::No)
22{
23}
24
25ErrorOr<void> ProjectBuilder::build(StringView active_file)
26{
27 m_terminal->clear_including_history();
28
29 if (auto command = m_project.config()->build_command(); command.has_value()) {
30 TRY(m_terminal->run_command(command.value()));
31 return {};
32 }
33
34 if (active_file.is_null())
35 return Error::from_string_literal("no active file");
36
37 if (active_file.ends_with(".js"sv)) {
38 TRY(m_terminal->run_command(DeprecatedString::formatted("js -A {}", active_file)));
39 return {};
40 }
41
42 if (m_is_serenity == IsSerenityRepo::No) {
43 TRY(verify_make_is_installed());
44 TRY(m_terminal->run_command("make"));
45 return {};
46 }
47
48 TRY(update_active_file(active_file));
49
50 return build_serenity_component();
51}
52
53ErrorOr<void> ProjectBuilder::run(StringView active_file)
54{
55 if (auto command = m_project.config()->run_command(); command.has_value()) {
56 TRY(m_terminal->run_command(command.value()));
57 return {};
58 }
59
60 if (active_file.is_null())
61 return Error::from_string_literal("no active file");
62
63 if (active_file.ends_with(".js"sv)) {
64 TRY(m_terminal->run_command(DeprecatedString::formatted("js {}", active_file)));
65 return {};
66 }
67
68 if (m_is_serenity == IsSerenityRepo::No) {
69 TRY(verify_make_is_installed());
70 TRY(m_terminal->run_command("make run"));
71 return {};
72 }
73
74 TRY(update_active_file(active_file));
75
76 return run_serenity_component();
77}
78
79ErrorOr<void> ProjectBuilder::run_serenity_component()
80{
81 auto relative_path_to_dir = LexicalPath::relative_path(LexicalPath::dirname(m_serenity_component_cmake_file), m_project_root);
82 TRY(m_terminal->run_command(LexicalPath::join(relative_path_to_dir, m_serenity_component_name).string(), build_directory()));
83 return {};
84}
85
86ErrorOr<void> ProjectBuilder::update_active_file(StringView active_file)
87{
88 TRY(verify_cmake_is_installed());
89 auto cmake_file = find_cmake_file_for(active_file);
90 if (!cmake_file.has_value()) {
91 warnln("did not find cmake file for: {}", active_file);
92 return Error::from_string_literal("did not find cmake file");
93 }
94
95 if (m_serenity_component_cmake_file == cmake_file.value())
96 return {};
97
98 m_serenity_component_cmake_file = cmake_file.value();
99 m_serenity_component_name = TRY(component_name(m_serenity_component_cmake_file));
100
101 TRY(initialize_build_directory());
102 return {};
103}
104
105ErrorOr<void> ProjectBuilder::build_serenity_component()
106{
107 TRY(verify_make_is_installed());
108 TRY(m_terminal->run_command(DeprecatedString::formatted("make {}", m_serenity_component_name), build_directory(), TerminalWrapper::WaitForExit::Yes, "Make failed"sv));
109 return {};
110}
111
112ErrorOr<DeprecatedString> ProjectBuilder::component_name(StringView cmake_file_path)
113{
114 auto file = TRY(Core::File::open(cmake_file_path, Core::File::OpenMode::Read));
115 auto content = TRY(file->read_until_eof());
116
117 static Regex<ECMA262> const component_name(R"~~~(serenity_component\([\s]*(\w+)[\s\S]*\))~~~");
118 RegexResult result;
119 if (!component_name.search(StringView { content }, result))
120 return Error::from_string_literal("component not found");
121
122 return DeprecatedString { result.capture_group_matches.at(0).at(0).view.string_view() };
123}
124
125ErrorOr<void> ProjectBuilder::initialize_build_directory()
126{
127 if (!Core::DeprecatedFile::exists(build_directory())) {
128 if (mkdir(LexicalPath::join(build_directory()).string().characters(), 0700)) {
129 return Error::from_errno(errno);
130 }
131 }
132
133 auto cmake_file_path = LexicalPath::join(build_directory(), "CMakeLists.txt"sv).string();
134 if (Core::DeprecatedFile::exists(cmake_file_path))
135 MUST(Core::DeprecatedFile::remove(cmake_file_path, Core::DeprecatedFile::RecursionMode::Disallowed));
136
137 auto cmake_file = TRY(Core::File::open(cmake_file_path, Core::File::OpenMode::Write));
138 TRY(cmake_file->write_until_depleted(generate_cmake_file_content().bytes()));
139
140 TRY(m_terminal->run_command(DeprecatedString::formatted("cmake -S {} -DHACKSTUDIO_BUILD=ON -DHACKSTUDIO_BUILD_CMAKE_FILE={}"
141 " -DENABLE_UNICODE_DATABASE_DOWNLOAD=OFF",
142 m_project_root, cmake_file_path),
143 build_directory(), TerminalWrapper::WaitForExit::Yes, "CMake error"sv));
144
145 return {};
146}
147
148Optional<DeprecatedString> ProjectBuilder::find_cmake_file_for(StringView file_path) const
149{
150 auto directory = LexicalPath::dirname(file_path);
151 while (!directory.is_empty()) {
152 auto cmake_path = LexicalPath::join(m_project_root, directory, "CMakeLists.txt"sv);
153 if (Core::DeprecatedFile::exists(cmake_path.string()))
154 return cmake_path.string();
155 directory = LexicalPath::dirname(directory);
156 }
157 return {};
158}
159
160DeprecatedString ProjectBuilder::generate_cmake_file_content() const
161{
162 StringBuilder builder;
163 builder.appendff("add_subdirectory({})\n", LexicalPath::dirname(m_serenity_component_cmake_file));
164
165 auto defined_libraries = get_defined_libraries();
166 for (auto& library : defined_libraries) {
167 builder.appendff("add_library({} SHARED IMPORTED GLOBAL)\n", library.key);
168 builder.appendff("set_target_properties({} PROPERTIES IMPORTED_LOCATION {})\n", library.key, library.value->path);
169
170 if (library.key == "LibCStaticWithoutDeps"sv || library.key == "DumpLayoutTree"sv)
171 continue;
172
173 // We need to specify the dependencies for each defined library in CMake because some applications do not specify
174 // all of their direct dependencies in the CMakeLists file.
175 // For example, a target may directly use LibGFX but only specify LibGUI as a dependency (which in turn depends on LibGFX).
176 // In this example, if we don't specify the dependencies of LibGUI in the CMake file, linking will fail because of undefined LibGFX symbols.
177 builder.appendff("target_link_libraries({} INTERFACE {})\n", library.key, DeprecatedString::join(' ', library.value->dependencies));
178 }
179
180 return builder.to_deprecated_string();
181}
182
183HashMap<DeprecatedString, NonnullOwnPtr<ProjectBuilder::LibraryInfo>> ProjectBuilder::get_defined_libraries()
184{
185 HashMap<DeprecatedString, NonnullOwnPtr<ProjectBuilder::LibraryInfo>> libraries;
186
187 for_each_library_definition([&libraries](DeprecatedString name, DeprecatedString path) {
188 libraries.set(name, make<ProjectBuilder::LibraryInfo>(move(path)));
189 });
190 for_each_library_dependencies([&libraries](DeprecatedString name, Vector<StringView> const& dependencies) {
191 auto library = libraries.get(name);
192 if (!library.has_value())
193 return;
194 for (auto const& dependency : dependencies) {
195 if (libraries.contains(dependency))
196 library.value()->dependencies.append(dependency);
197 }
198 });
199 return libraries;
200}
201
202void ProjectBuilder::for_each_library_definition(Function<void(DeprecatedString, DeprecatedString)> func)
203{
204 Vector<DeprecatedString> arguments = { "-c", "find Userland -name CMakeLists.txt | xargs grep serenity_lib" };
205 auto res = Core::command("/bin/sh", arguments, {});
206 if (res.is_error()) {
207 warnln("{}", res.error());
208 return;
209 }
210
211 static Regex<ECMA262> const parse_library_definition(R"~~~(.+:serenity_lib[c]?\((\w+) (\w+)\).*)~~~");
212 for (auto& line : res.value().output.split('\n')) {
213 RegexResult result;
214 if (!parse_library_definition.search(line, result))
215 continue;
216 if (result.capture_group_matches.size() != 1 || result.capture_group_matches[0].size() != 2)
217 continue;
218
219 auto library_name = result.capture_group_matches.at(0).at(0).view.string_view();
220 auto library_obj_name = result.capture_group_matches.at(0).at(1).view.string_view();
221 auto so_path = DeprecatedString::formatted("{}.so", LexicalPath::join("/usr/lib"sv, DeprecatedString::formatted("lib{}", library_obj_name)).string());
222 func(library_name, so_path);
223 }
224
225 // ssp is defined with "add_library" so it doesn't get picked up with the current logic for finding library definitions.
226 func("ssp", "/usr/lib/libssp.a");
227}
228
229void ProjectBuilder::for_each_library_dependencies(Function<void(DeprecatedString, Vector<StringView>)> func)
230{
231 Vector<DeprecatedString> arguments = { "-c", "find Userland/Libraries -name CMakeLists.txt | xargs grep target_link_libraries" };
232 auto res = Core::command("/bin/sh", arguments, {});
233 if (res.is_error()) {
234 warnln("{}", res.error());
235 return;
236 }
237
238 static Regex<ECMA262> const parse_library_definition(R"~~~(.+:target_link_libraries\((\w+) ([\w\s]+)\).*)~~~");
239 for (auto& line : res.value().output.split('\n')) {
240
241 RegexResult result;
242 if (!parse_library_definition.search(line, result))
243 continue;
244 if (result.capture_group_matches.size() != 1 || result.capture_group_matches[0].size() != 2)
245 continue;
246
247 auto library_name = result.capture_group_matches.at(0).at(0).view.string_view();
248 auto dependencies_string = result.capture_group_matches.at(0).at(1).view.string_view();
249
250 func(library_name, dependencies_string.split_view(' '));
251 }
252}
253
254ErrorOr<void> ProjectBuilder::verify_cmake_is_installed()
255{
256 auto res = Core::command("cmake --version", {});
257 if (!res.is_error() && res.value().exit_code == 0)
258 return {};
259 return Error::from_string_literal("CMake port is not installed");
260}
261
262ErrorOr<void> ProjectBuilder::verify_make_is_installed()
263{
264 auto res = Core::command("make --version", {});
265 if (!res.is_error() && res.value().exit_code == 0)
266 return {};
267 return Error::from_string_literal("Make port is not installed");
268}
269
270DeprecatedString ProjectBuilder::build_directory() const
271{
272 return LexicalPath::join(m_project_root, "Build"sv).string();
273}
274
275}