Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport the python3 embedSdf script variant #1240

Merged
merged 3 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions sdf/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,31 @@ add_subdirectory(1.10)
add_custom_target(schema)
add_dependencies(schema schema1_10)

# Optionally use the python script (default in sdf14+)
if (EMBEDSDF_PY)
if (NOT Python3_Interpreter_FOUND)
gz_build_error("Python is required to generate the C++ file with the SDF content")
endif()

# Generate the EmbeddedSdf.cc file, which contains all the supported SDF
# descriptions in a map of strings. The parser.cc file uses EmbeddedSdf.hh.
execute_process(
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/sdf/embedSdf.rb
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/sdf"
OUTPUT_FILE "${PROJECT_BINARY_DIR}/src/EmbeddedSdf.cc"
)
execute_process(
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/sdf/embedSdf.py
--output-file "${PROJECT_BINARY_DIR}/src/EmbeddedSdf.cc"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/sdf"
OUTPUT_FILE "${PROJECT_BINARY_DIR}/src/EmbeddedSdf.cc"
)
else()
execute_process(
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/sdf/embedSdf.rb
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/sdf"
OUTPUT_FILE "${PROJECT_BINARY_DIR}/src/EmbeddedSdf.cc"
)
endif()


# Generate aggregated SDF description files for use by the sdformat.org
# website. If the description files change, the generated full*.sdf files need
# Generate aggregated SDF description files for use by the sdformat.org
# website. If the description files change, the generated full*.sdf files need
# to be removed before running this target.
if (GZ_PROGRAM)

Expand Down
203 changes: 203 additions & 0 deletions sdf/embedSdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""
Script for generating a C++ file that contains the content from all SDF files
"""

from typing import List, Optional

import argparse
import inspect
import sys
from pathlib import Path


# The list of supported SDF specification versions. This will let us drop
# versions without removing the directories.
SUPPORTED_SDF_VERSIONS = [
"1.10",
"1.9",
"1.8",
"1.7",
"1.6",
"1.5",
"1.4",
"1.3",
"1.2",
]

# The list of supported SDF conversions. This list includes versions that
# a user can convert an existing SDF version to.
SUPPORTED_SDF_CONVERSIONS = ["1.10", "1.9", "1.8", "1.7", "1.6", "1.5", "1.4", "1.3"]

# whitespace indentation for C++ code
INDENTATION = " "

# newline character
NEWLINE = "\n"


def get_copyright_notice() -> str:
"""
Provides the copyright notice for the C++ file

:returns: copyright notice
"""
res = inspect.cleandoc(
"""
/*
* Copyright 2022 Open Source Robotics Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
"""
)
return res + 2 * NEWLINE


def get_file_header_prolog() -> str:
"""
Provides the include statement, namespace and variable declaration of the C++ file

:returns: prolog of the C++ file
"""
res = inspect.cleandoc(
"""
#include "EmbeddedSdf.hh"

namespace sdf
{
inline namespace SDF_VERSION_NAMESPACE
{
/////////////////////////////////////////////////
const std::map<std::string, std::string> &GetEmbeddedSdf()
{
static const std::map<std::string, std::string> result {
"""
)
return res + NEWLINE


def embed_sdf_content(arg_path: str, arg_file_content: str) -> str:
"""
Generates a string pair with the folder and filename
as well as the content of the file

:param arg_path: Foldername and filename of the SDF
:param arg_file_content: Content of the provided file
:returns: raw string literal mapping pair for the std::map
"""
res = []
res.append("// NOLINT")
res.append("{")
res.append(f'"{arg_path}",')
res.append('R"__sdf_literal__(')
res.append(f"{arg_file_content}")
res.append(')__sdf_literal__"')
res.append("}")
return NEWLINE.join(res)


def get_file_header_epilog() -> str:
"""
Provides the return statement and the closing brackets of the C++ file

:returns: epilog of the C++ file
"""
res = inspect.cleandoc(
"""
};

return result;
}

}
} // namespace sdf

"""
)
return NEWLINE + res


def write_output(file_content: str, output_filename: str) -> None:
"""
Print the content of the EmbeddedSdf.cc to a file
"""
copyright_notice = get_copyright_notice()
prolog = get_file_header_prolog()
epilog = get_file_header_epilog()
output_content = copyright_notice + prolog + file_content + epilog

with open(output_filename, "w", encoding="utf8") as output_file:
output_file.write(output_content)


def collect_file_locations() -> List[Path]:
paths: List[Path] = []

for sdf_version in SUPPORTED_SDF_VERSIONS:
paths.extend(Path(sdf_version).glob("*.sdf"))
for sdf_conversion in SUPPORTED_SDF_CONVERSIONS:
paths.extend(Path(sdf_conversion).glob("*.convert"))
return paths


def generate_map_content(paths: List[Path], relative_to: Optional[str] = None) -> str:
'''
Generate the EmbeddedSdf.cc content
'''
content = []
for path in paths:
with open(path, "r", encoding="utf8") as input_sdf:
file_content = input_sdf.read()

# Strip relative path if requested
if relative_to is not None:
path = path.relative_to(relative_to)
content.append(embed_sdf_content(str(path), file_content))
return ",".join(content)


def main(args=None) -> int:
'''
Main entrypoint
'''
if args is None:
args = sys.argv[1:]

parser = argparse.ArgumentParser()

parser.add_argument(
"--sdf-root",
default=None,
help="Directory containing sdf description files for each version",
)
parser.add_argument(
"--input-files", nargs="*", help="List of input files to be embedded"
)
parser.add_argument(
"--output-file", help="File to output embeddedsdf.cc content to"
)

args = parser.parse_args(args)
if not args.input_files or len(args.input_files) == 0:
paths = collect_file_locations()
else:
paths = [Path(f) for f in args.input_files]
content = generate_map_content(paths, args.sdf_root)
write_output(content, args.output_file)
return 0


if __name__ == "__main__":
sys.exit(main())
56 changes: 56 additions & 0 deletions src/SDF_TEST.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
#include <gz/utils/Environment.hh>
#include <gz/utils/SuppressWarning.hh>

#include "test_config.hh"
#include "test_utils.hh"

#include "sdf/sdf.hh"

class SDFUpdateFixture
Expand Down Expand Up @@ -559,6 +562,59 @@ TEST(SDF, Version)
EXPECT_STREQ(SDF_VERSION, sdf::SDF::Version().c_str());
}

/////////////////////////////////////////////////
TEST(SDF, EmbeddedSpec)
{
std::string result;

result = sdf::SDF::EmbeddedSpec("actor.sdf", false);
EXPECT_NE(result.find("<!-- Actor -->"), std::string::npos);
EXPECT_NE(result.find("<element name=\"actor\" required=\"*\">"),
std::string::npos);
result = sdf::SDF::EmbeddedSpec("actor.sdf", true);
EXPECT_NE(result.find("<!-- Actor -->"), std::string::npos);
EXPECT_NE(result.find("<element name=\"actor\" required=\"*\">"),
std::string::npos);

result = sdf::SDF::EmbeddedSpec("root.sdf", false);
EXPECT_NE(result.find("SDFormat base element"), std::string::npos);
EXPECT_NE(result.find("name=\"version\" type=\"string\""), std::string::npos);
result = sdf::SDF::EmbeddedSpec("root.sdf", true);
EXPECT_NE(result.find("SDFormat base element"), std::string::npos);
EXPECT_NE(result.find("name=\"version\" type=\"string\""), std::string::npos);
}

TEST(SDF, EmbeddedSpecNonExistent)
{
std::string result;

// Capture sdferr output
std::stringstream stderr_buffer;
sdf::testing::RedirectConsoleStream redir(
sdf::Console::Instance()->GetMsgStream(), &stderr_buffer);
#ifdef _WIN32
sdf::Console::Instance()->SetQuiet(false);
sdf::testing::ScopeExit revertSetQuiet(
[]
{
sdf::Console::Instance()->SetQuiet(true);
});
#endif

result = sdf::SDF::EmbeddedSpec("unavailable.sdf", false);
EXPECT_NE(stderr_buffer.str().find("Unable to find SDF filename"),
std::string::npos);
EXPECT_NE(stderr_buffer.str().find("with version"), std::string::npos);
EXPECT_TRUE(result.empty());

// clear the contents of the buffer
stderr_buffer.str("");

result = sdf::SDF::EmbeddedSpec("unavailable.sdf", true);
EXPECT_TRUE(stderr_buffer.str().empty());
EXPECT_TRUE(result.empty());
}

/////////////////////////////////////////////////
TEST(SDF, FilePath)
{
Expand Down