Commit 54d76753 authored by brettw's avatar brettw Committed by Commit bot

Propagate GN public shared libraries through shared libraries

Previously all dependencies would stop propagating for link purposes when a shared library or executable boundary is reached.

This patch makes public shared library dependencies propagate through shared libraries. Since the ability to use header files is propagated through these boundaries, so do the link dependencies. See the comment in target.cc for more details.

BUG=475091

Review URL: https://codereview.chromium.org/1083663007

Cr-Commit-Position: refs/heads/master@{#325471}
parent dd12e9d5
......@@ -77,6 +77,8 @@ static_library("gn_lib") {
"header_checker.h",
"import_manager.cc",
"import_manager.h",
"inherited_libraries.cc",
"inherited_libraries.h",
"input_conversion.cc",
"input_conversion.h",
"input_file.cc",
......@@ -240,6 +242,7 @@ test("gn_unittests") {
"functions_target_unittest.cc",
"functions_unittest.cc",
"header_checker_unittest.cc",
"inherited_libraries_unittest.cc",
"input_conversion_unittest.cc",
"label_pattern_unittest.cc",
"label_unittest.cc",
......
......@@ -79,6 +79,8 @@
'header_checker.h',
'import_manager.cc',
'import_manager.h',
'inherited_libraries.cc',
'inherited_libraries.h',
'input_conversion.cc',
'input_conversion.h',
'input_file.cc',
......@@ -217,6 +219,7 @@
'functions_target_unittest.cc',
'functions_unittest.cc',
'header_checker_unittest.cc',
'inherited_libraries_unittest.cc',
'input_conversion_unittest.cc',
'label_pattern_unittest.cc',
'label_unittest.cc',
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "tools/gn/inherited_libraries.h"
#include "tools/gn/target.h"
InheritedLibraries::InheritedLibraries() {
}
InheritedLibraries::~InheritedLibraries() {
}
std::vector<const Target*> InheritedLibraries::GetOrdered() const {
std::vector<const Target*> result;
result.resize(map_.size());
// The indices in the map should be from 0 to the number of items in the
// map, so insert directly into the result (with some sanity checks).
for (const auto& pair : map_) {
size_t index = pair.second.index;
DCHECK(index < result.size());
DCHECK(!result[index]);
result[index] = pair.first;
}
return result;
}
std::vector<std::pair<const Target*, bool>>
InheritedLibraries::GetOrderedAndPublicFlag() const {
std::vector<std::pair<const Target*, bool>> result;
result.resize(map_.size());
for (const auto& pair : map_) {
size_t index = pair.second.index;
DCHECK(index < result.size());
DCHECK(!result[index].first);
result[index] = std::make_pair(pair.first, pair.second.is_public);
}
return result;
}
void InheritedLibraries::Append(const Target* target, bool is_public) {
// Try to insert a new node.
auto insert_result = map_.insert(
std::make_pair(target, Node(map_.size(), is_public)));
if (!insert_result.second) {
// Element already present, insert failed and insert_result indicates the
// old one. The old one may need to have its public flag updated.
if (is_public) {
Node& existing_node = insert_result.first->second;
existing_node.is_public = true;
}
}
}
void InheritedLibraries::AppendInherited(const InheritedLibraries& other,
bool is_public) {
// Append all items in order, mark them public only if the're already public
// and we're adding them publically.
for (const auto& cur : other.GetOrderedAndPublicFlag())
Append(cur.first, is_public && cur.second);
}
void InheritedLibraries::AppendPublicSharedLibraries(
const InheritedLibraries& other,
bool is_public) {
for (const auto& cur : other.GetOrderedAndPublicFlag()) {
if (cur.first->output_type() == Target::SHARED_LIBRARY && cur.second)
Append(cur.first, is_public);
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef TOOLS_GN_INHERITED_LIBRARIES_H_
#define TOOLS_GN_INHERITED_LIBRARIES_H_
#include <map>
#include <utility>
#include <vector>
#include "base/basictypes.h"
class Target;
// Represents an ordered uniquified set of all shared/static libraries for
// a given target. These are pushed up the dependency tree.
//
// Maintaining the order is important so GN links all libraries in the same
// order specified in the build files.
//
// Since this list is uniquified, appending to the list will not actually
// append a new item if the target already exists. However, the existing one
// may have its is_public flag updated. "Public" always wins, so is_public will
// be true if any dependency with that name has been set to public.
class InheritedLibraries {
public:
InheritedLibraries();
~InheritedLibraries();
// Returns the list of dependencies in order, optionally with the flag
// indicating whether the dependency is public.
std::vector<const Target*> GetOrdered() const;
std::vector<std::pair<const Target*, bool>> GetOrderedAndPublicFlag() const;
// Adds a single dependency to the end of the list. See note on adding above.
void Append(const Target* target, bool is_public);
// Appends all items from the "other" list to the current one. The is_public
// parameter indicates how the current target depends on the items in
// "other". If is_public is true, the existing public flags of the appended
// items will be preserved (propogating the public-ness up the dependency
// chain). If is_public is false, all deps will be added as private since
// the current target isn't forwarding them.
void AppendInherited(const InheritedLibraries& other, bool is_public);
// Like AppendInherited but only appends the items in "other" that are of
// type SHARED_LIBRARY and only when they're marked public. This is used
// to push shared libraries up the dependency chain, following only public
// deps, to dependent targets that need to use them.
void AppendPublicSharedLibraries(const InheritedLibraries& other,
bool is_public);
private:
struct Node {
Node() : index(static_cast<size_t>(-1)), is_public(false) {}
Node(size_t i, bool p) : index(i), is_public(p) {}
size_t index;
bool is_public;
};
typedef std::map<const Target*, Node> LibraryMap;
LibraryMap map_;
DISALLOW_COPY_AND_ASSIGN(InheritedLibraries);
};
#endif // TOOLS_GN_INHERITED_LIBRARIES_H_
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "testing/gtest/include/gtest/gtest.h"
#include "tools/gn/inherited_libraries.h"
#include "tools/gn/target.h"
#include "tools/gn/test_with_scope.h"
namespace {
// In these tests, Pair can't be used conveniently because the
// "const" won't be inferred and the types won't match. This helper makes the
// right type of pair with the const Target.
std::pair<const Target*, bool> Pair(const Target* t, bool b) {
return std::pair<const Target*, bool>(t, b);
}
} // namespace
TEST(InheritedLibraries, Unique) {
TestWithScope setup;
Target a(setup.settings(), Label(SourceDir("//foo/"), "a"));
Target b(setup.settings(), Label(SourceDir("//foo/"), "b"));
// Setup, add the two targets as private.
InheritedLibraries libs;
libs.Append(&a, false);
libs.Append(&b, false);
auto result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(2u, result.size());
EXPECT_EQ(Pair(&a, false), result[0]);
EXPECT_EQ(Pair(&b, false), result[1]);
// Add again as private, this should be a NOP.
libs.Append(&a, false);
libs.Append(&b, false);
result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(2u, result.size());
EXPECT_EQ(Pair(&a, false), result[0]);
EXPECT_EQ(Pair(&b, false), result[1]);
// Add as public, this should make both public.
libs.Append(&a, true);
libs.Append(&b, true);
result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(2u, result.size());
EXPECT_EQ(Pair(&a, true), result[0]);
EXPECT_EQ(Pair(&b, true), result[1]);
// Add again private, they should stay public.
libs.Append(&a, false);
libs.Append(&b, false);
result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(2u, result.size());
EXPECT_EQ(Pair(&a, true), result[0]);
EXPECT_EQ(Pair(&b, true), result[1]);
}
TEST(InheritedLibraries, AppendInherited) {
TestWithScope setup;
Target a(setup.settings(), Label(SourceDir("//foo/"), "a"));
Target b(setup.settings(), Label(SourceDir("//foo/"), "b"));
Target w(setup.settings(), Label(SourceDir("//foo/"), "w"));
Target x(setup.settings(), Label(SourceDir("//foo/"), "x"));
Target y(setup.settings(), Label(SourceDir("//foo/"), "y"));
Target z(setup.settings(), Label(SourceDir("//foo/"), "z"));
InheritedLibraries libs;
libs.Append(&a, false);
libs.Append(&b, false);
// Appending these things with private inheritance should make them private,
// no matter how they're listed in the appended class.
InheritedLibraries append_private;
append_private.Append(&a, true);
append_private.Append(&b, false);
append_private.Append(&w, true);
append_private.Append(&x, false);
libs.AppendInherited(append_private, false);
auto result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(4u, result.size());
EXPECT_EQ(Pair(&a, false), result[0]);
EXPECT_EQ(Pair(&b, false), result[1]);
EXPECT_EQ(Pair(&w, false), result[2]);
EXPECT_EQ(Pair(&x, false), result[3]);
// Appending these things with public inheritance should convert them.
InheritedLibraries append_public;
append_public.Append(&a, true);
append_public.Append(&b, false);
append_public.Append(&y, true);
append_public.Append(&z, false);
libs.AppendInherited(append_public, true);
result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(6u, result.size());
EXPECT_EQ(Pair(&a, true), result[0]); // Converted to public.
EXPECT_EQ(Pair(&b, false), result[1]);
EXPECT_EQ(Pair(&w, false), result[2]);
EXPECT_EQ(Pair(&x, false), result[3]);
EXPECT_EQ(Pair(&y, true), result[4]); // Appended as public.
EXPECT_EQ(Pair(&z, false), result[5]);
}
TEST(InheritedLibraries, AppendPublicSharedLibraries) {
TestWithScope setup;
InheritedLibraries append;
// Two source sets.
Target set_pub(setup.settings(), Label(SourceDir("//foo/"), "set_pub"));
set_pub.set_output_type(Target::SOURCE_SET);
append.Append(&set_pub, true);
Target set_priv(setup.settings(), Label(SourceDir("//foo/"), "set_priv"));
set_priv.set_output_type(Target::SOURCE_SET);
append.Append(&set_priv, false);
// Two shared libraries.
Target sh_pub(setup.settings(), Label(SourceDir("//foo/"), "sh_pub"));
sh_pub.set_output_type(Target::SHARED_LIBRARY);
append.Append(&sh_pub, true);
Target sh_priv(setup.settings(), Label(SourceDir("//foo/"), "sh_priv"));
sh_priv.set_output_type(Target::SHARED_LIBRARY);
append.Append(&sh_priv, false);
InheritedLibraries libs;
libs.AppendPublicSharedLibraries(append, true);
auto result = libs.GetOrderedAndPublicFlag();
ASSERT_EQ(1u, result.size());
EXPECT_EQ(Pair(&sh_pub, true), result[0]);
}
......@@ -383,7 +383,8 @@ void NinjaBinaryTargetWriter::GetDeps(
}
// Inherited libraries.
for (const auto& inherited_target : target_->inherited_libraries()) {
for (const auto& inherited_target :
target_->inherited_libraries().GetOrdered()) {
ClassifyDependency(inherited_target, extra_object_files,
linkable_deps, non_linkable_deps);
}
......
......@@ -126,7 +126,7 @@ bool Target::OnResolved(Err* err) {
all_libs_.append(cur.libs().begin(), cur.libs().end());
}
PullDependentTargetInfo();
PullDependentTargets();
PullForwardedDependentConfigs();
PullRecursiveHardDeps();
......@@ -208,32 +208,56 @@ bool Target::SetToolchain(const Toolchain* toolchain, Err* err) {
return false;
}
void Target::PullDependentTargetInfo() {
// Gather info from our dependents we need.
for (const auto& pair : GetDeps(DEPS_LINKED)) {
const Target* dep = pair.ptr;
MergeAllDependentConfigsFrom(dep, &configs_, &all_dependent_configs_);
MergePublicConfigsFrom(dep, &configs_);
// Direct dependent libraries.
if (dep->output_type() == STATIC_LIBRARY ||
dep->output_type() == SHARED_LIBRARY ||
dep->output_type() == SOURCE_SET)
inherited_libraries_.push_back(dep);
// Inherited libraries and flags are inherited across static library
// boundaries.
if (!dep->IsFinal()) {
inherited_libraries_.Append(dep->inherited_libraries().begin(),
dep->inherited_libraries().end());
// Inherited library settings.
all_lib_dirs_.append(dep->all_lib_dirs());
all_libs_.append(dep->all_libs());
}
void Target::PullDependentTarget(const Target* dep, bool is_public) {
MergeAllDependentConfigsFrom(dep, &configs_, &all_dependent_configs_);
MergePublicConfigsFrom(dep, &configs_);
// Direct dependent libraries.
if (dep->output_type() == STATIC_LIBRARY ||
dep->output_type() == SHARED_LIBRARY ||
dep->output_type() == SOURCE_SET)
inherited_libraries_.Append(dep, is_public);
if (dep->output_type() == SHARED_LIBRARY) {
// Shared library dependendencies are inherited across public shared
// library boundaries.
//
// In this case:
// EXE -> INTERMEDIATE_SHLIB --[public]--> FINAL_SHLIB
// The EXE will also link to to FINAL_SHLIB. The public dependeny means
// that the EXE can use the headers in FINAL_SHLIB so the FINAL_SHLIB
// will need to appear on EXE's link line.
//
// However, if the dependency is private:
// EXE -> INTERMEDIATE_SHLIB --[private]--> FINAL_SHLIB
// the dependency will not be propogated because INTERMEDIATE_SHLIB is
// not granting permission to call functiosn from FINAL_SHLIB. If EXE
// wants to use functions (and link to) FINAL_SHLIB, it will need to do
// so explicitly.
//
// Static libraries and source sets aren't inherited across shared
// library boundaries because they will be linked into the shared
// library.
inherited_libraries_.AppendPublicSharedLibraries(
dep->inherited_libraries(), is_public);
} else if (!dep->IsFinal()) {
// The current target isn't linked, so propogate linked deps and
// libraries up the dependency tree.
inherited_libraries_.AppendInherited(dep->inherited_libraries(), is_public);
// Inherited library settings.
all_lib_dirs_.append(dep->all_lib_dirs());
all_libs_.append(dep->all_libs());
}
}
void Target::PullDependentTargets() {
for (const auto& dep : public_deps_)
PullDependentTarget(dep.ptr, true);
for (const auto& dep : private_deps_)
PullDependentTarget(dep.ptr, false);
}
void Target::PullForwardedDependentConfigs() {
// Pull public configs from each of our dependency's public deps.
for (const auto& dep : public_deps_)
......@@ -377,7 +401,7 @@ bool Target::CheckNoNestedStaticLibs(Err* err) const {
}
// Verify no inherited libraries are static libraries.
for (const auto& lib : inherited_libraries()) {
for (const auto& lib : inherited_libraries().GetOrdered()) {
if (lib->output_type() == Target::STATIC_LIBRARY) {
*err = MakeStaticLibDepsError(this, lib);
return false;
......
......@@ -15,6 +15,7 @@
#include "base/synchronization/lock.h"
#include "tools/gn/action_values.h"
#include "tools/gn/config_values.h"
#include "tools/gn/inherited_libraries.h"
#include "tools/gn/item.h"
#include "tools/gn/label_ptr.h"
#include "tools/gn/ordered_set.h"
......@@ -189,7 +190,7 @@ class Target : public Item {
return allow_circular_includes_from_;
}
const UniqueVector<const Target*>& inherited_libraries() const {
const InheritedLibraries& inherited_libraries() const {
return inherited_libraries_;
}
......@@ -241,7 +242,8 @@ class Target : public Item {
private:
// Pulls necessary information from dependencies to this one when all
// dependencies have been resolved.
void PullDependentTargetInfo();
void PullDependentTarget(const Target* dep, bool is_public);
void PullDependentTargets();
// These each pull specific things from dependencies to this one when all
// deps have been resolved.
......@@ -281,12 +283,9 @@ class Target : public Item {
std::set<Label> allow_circular_includes_from_;
// Static libraries and source sets from transitive deps. These things need
// to be linked only with the end target (executable, shared library). Source
// sets do not get pushed beyond static library boundaries, and neither
// source sets nor static libraries get pushed beyond sahred library
// boundaries.
UniqueVector<const Target*> inherited_libraries_;
// Static libraries, shared libraries, and source sets from transitive deps
// that need to be linked.
InheritedLibraries inherited_libraries_;
// These libs and dirs are inherited from statically linked deps and all
// configs applying to this target.
......
......@@ -170,21 +170,21 @@ TEST(Target, InheritLibs) {
ASSERT_TRUE(a.OnResolved(&err));
// C should have D in its inherited libs.
const UniqueVector<const Target*>& c_inherited = c.inherited_libraries();
EXPECT_EQ(1u, c_inherited.size());
EXPECT_TRUE(c_inherited.IndexOf(&d) != static_cast<size_t>(-1));
std::vector<const Target*> c_inherited = c.inherited_libraries().GetOrdered();
ASSERT_EQ(1u, c_inherited.size());
EXPECT_EQ(&d, c_inherited[0]);
// B should have C and D in its inherited libs.
const UniqueVector<const Target*>& b_inherited = b.inherited_libraries();
EXPECT_EQ(2u, b_inherited.size());
EXPECT_TRUE(b_inherited.IndexOf(&c) != static_cast<size_t>(-1));
EXPECT_TRUE(b_inherited.IndexOf(&d) != static_cast<size_t>(-1));
std::vector<const Target*> b_inherited = b.inherited_libraries().GetOrdered();
ASSERT_EQ(2u, b_inherited.size());
EXPECT_EQ(&c, b_inherited[0]);
EXPECT_EQ(&d, b_inherited[1]);
// A should have B in its inherited libs, but not any others (the shared
// library will include the static library and source set).
const UniqueVector<const Target*>& a_inherited = a.inherited_libraries();
EXPECT_EQ(1u, a_inherited.size());
EXPECT_TRUE(a_inherited.IndexOf(&b) != static_cast<size_t>(-1));
std::vector<const Target*> a_inherited = a.inherited_libraries().GetOrdered();
ASSERT_EQ(1u, a_inherited.size());
EXPECT_EQ(&b, a_inherited[0]);
}
TEST(Target, InheritCompleteStaticLib) {
......@@ -214,15 +214,15 @@ TEST(Target, InheritCompleteStaticLib) {
ASSERT_TRUE(a.OnResolved(&err));
// B should have C in its inherited libs.
const UniqueVector<const Target*>& b_inherited = b.inherited_libraries();
EXPECT_EQ(1u, b_inherited.size());
EXPECT_TRUE(b_inherited.IndexOf(&c) != static_cast<size_t>(-1));
std::vector<const Target*> b_inherited = b.inherited_libraries().GetOrdered();
ASSERT_EQ(1u, b_inherited.size());
EXPECT_EQ(&c, b_inherited[0]);
// A should have B in its inherited libs, but not any others (the complete
// static library will include the source set).
const UniqueVector<const Target*>& a_inherited = a.inherited_libraries();
std::vector<const Target*> a_inherited = a.inherited_libraries().GetOrdered();
EXPECT_EQ(1u, a_inherited.size());
EXPECT_TRUE(a_inherited.IndexOf(&b) != static_cast<size_t>(-1));
EXPECT_EQ(&b, a_inherited[0]);
}
TEST(Target, InheritCompleteStaticLibNoDirectStaticLibDeps) {
......@@ -516,3 +516,57 @@ TEST(Target, LinkAndDepOutputs) {
EXPECT_EQ("./liba.so", target.link_output_file().value());
EXPECT_EQ("./liba.so.TOC", target.dependency_output_file().value());
}
// Shared libraries should be inherited across public shared liobrary
// boundaries.
TEST(Target, SharedInheritance) {
TestWithScope setup;
Err err;
// Create two leaf shared libraries.
Target pub(setup.settings(), Label(SourceDir("//foo/"), "pub"));
pub.set_output_type(Target::SHARED_LIBRARY);
pub.visibility().SetPublic();
pub.SetToolchain(setup.toolchain());
ASSERT_TRUE(pub.OnResolved(&err));
Target priv(setup.settings(), Label(SourceDir("//foo/"), "priv"));
priv.set_output_type(Target::SHARED_LIBRARY);
priv.visibility().SetPublic();
priv.SetToolchain(setup.toolchain());
ASSERT_TRUE(priv.OnResolved(&err));
// Intermediate shared library with the leaf shared libraries as
// dependencies, one public, one private.
Target inter(setup.settings(), Label(SourceDir("//foo/"), "inter"));
inter.set_output_type(Target::SHARED_LIBRARY);
inter.visibility().SetPublic();
inter.public_deps().push_back(LabelTargetPair(&pub));
inter.private_deps().push_back(LabelTargetPair(&priv));
inter.SetToolchain(setup.toolchain());
ASSERT_TRUE(inter.OnResolved(&err));
// The intermediate shared library should have both "pub" and "priv" in its
// inherited libraries.
std::vector<const Target*> inter_inherited =
inter.inherited_libraries().GetOrdered();
ASSERT_EQ(2u, inter_inherited.size());
EXPECT_EQ(&pub, inter_inherited[0]);
EXPECT_EQ(&priv, inter_inherited[1]);
// Make a toplevel executable target depending on the intermediate one.
Target exe(setup.settings(), Label(SourceDir("//foo/"), "exe"));
exe.set_output_type(Target::SHARED_LIBRARY);
exe.visibility().SetPublic();
exe.private_deps().push_back(LabelTargetPair(&inter));
exe.SetToolchain(setup.toolchain());
ASSERT_TRUE(exe.OnResolved(&err));
// The exe's inherited libraries should be "inter" (because it depended
// directly on it) and "pub" (because inter depended publicly on it).
std::vector<const Target*> exe_inherited =
exe.inherited_libraries().GetOrdered();
ASSERT_EQ(2u, exe_inherited.size());
EXPECT_EQ(&inter, exe_inherited[0]);
EXPECT_EQ(&pub, exe_inherited[1]);
}
......@@ -319,9 +319,8 @@ const char kTargetOutDir_Help[] =
" the \"deps\" list. This is done recursively. If a config appears\n" \
" more than once, only the first occurance will be used.\n" \
" 6. public_configs pulled from dependencies, in the order of the\n" \
" \"deps\" list. If a dependency has " \
"\"forward_dependent_configs_from\",\n" \
" or are public dependencies, they will be applied recursively.\n"
" \"deps\" list. If a dependency is public, they will be applied\n" \
" recursively.\n"
const char kAllDependentConfigs[] = "all_dependent_configs";
const char kAllDependentConfigs_HelpShort[] =
......@@ -612,14 +611,17 @@ const char kDeps_Help[] =
"\n"
" See also \"public_deps\" and \"data_deps\".\n";
// TODO(brettw) remove this, deprecated.
const char kForwardDependentConfigsFrom[] = "forward_dependent_configs_from";
const char kForwardDependentConfigsFrom_HelpShort[] =
"forward_dependent_configs_from: [label list] Forward dependent's configs.";
"forward_dependent_configs_from: [label list] DEPRECATED.";
const char kForwardDependentConfigsFrom_Help[] =
"forward_dependent_configs_from\n"
"\n"
" A list of target labels.\n"
"\n"
" DEPRECATED. Use public_deps instead which will have the same effect.\n"
"\n"
" Exposes the public_configs from a private dependent target as\n"
" public_configs of the current one. Each label in this list\n"
" must also be in the deps.\n"
......@@ -907,15 +909,18 @@ const char kPublicDeps_Help[] =
" additionally express that the current target exposes the listed deps\n"
" as part of its public API.\n"
"\n"
" This has two ramifications:\n"
" This has several ramifications:\n"
"\n"
" - public_configs that are part of the dependency are forwarded\n"
" to direct dependents (this is the same as using\n"
" forward_dependent_configs_from).\n"
" to direct dependents.\n"
"\n"
" - public headers in the dependency are usable by dependents\n"
" - Public headers in the dependency are usable by dependents\n"
" (includes do not require a direct dependency or visibility).\n"
"\n"
" - If the current target is a shared library, other shared libraries\n"
" that it publicly depends on (directly or indirectly) are\n"
" propagated up the dependency tree to dependents for linking.\n"
"\n"
"Discussion\n"
"\n"
" Say you have three targets: A -> B -> C. C's visibility may allow\n"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment