Add mechanism for creating project-specific aliases for built-in checks
Specifically, the desired aliases can be specified as key-value pairs in the autoconf.aliases map with key being the new name and the value -- old/existing. See the README for details.
This commit is contained in:
parent
3a324ad757
commit
367c4529d2
116
README.md
116
README.md
@ -76,8 +76,8 @@ h{config}: in{config}
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As key-value pairs in the `autoconf.substitutions` map (which is an alias for
|
Or as key-value pairs in the `autoconf.substitutions` map (which is an alias
|
||||||
the `in.substitutions` variable; see the [`in`][module-in] module for
|
for the `in.substitutions` variable; see the [`in`][module-in] module for
|
||||||
details):
|
details):
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -113,10 +113,74 @@ h{config}: in{config}
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that an implementation of a check may depend on another check. As a
|
While this module provides widely used aliases for some checks, it doesn't
|
||||||
result, substitutions should not be conditional at the preprocessor level
|
attempt to cover every project's idiosyncrasies. Instead, it provides a
|
||||||
(unless all the checks are part of the same condition). Nor should the
|
mechanism for creating project-specific aliases for built-in
|
||||||
results of checks be adjusted until after the last check. For example:
|
checks. Specifically, the desired aliases can be specified as key-value pairs
|
||||||
|
in the `autoconf.aliases` map with key being the new name and the value --
|
||||||
|
old/existing. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
/* config.h.in */
|
||||||
|
|
||||||
|
#undef HAVE_AF_UNIX_H
|
||||||
|
#undef MY_SSIZE_T
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
h{config}: in{config}
|
||||||
|
{
|
||||||
|
autoconf.aliases = HAVE_AF_UNIX_H@HAVE_AFUNIX_H
|
||||||
|
autoconf.aliases += MY_SSIZE_T@ssize_t
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The built-in checks can be prefixed in order to avoid clashes with similarly
|
||||||
|
named macros in other headers. This is an especially good idea if the
|
||||||
|
resulting header is public. To enable this, we specify the prefix with
|
||||||
|
the `autoconf.prefix` variable and then use the prefixed versions of
|
||||||
|
the options in the `config.h.in` file. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
/* config.h.in */
|
||||||
|
|
||||||
|
#undef LIBFOO_HAVE_STRLCPY
|
||||||
|
#undef LIBFOO_HAVE_STRLCAT
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
h{config}: in{config}
|
||||||
|
{
|
||||||
|
autoconf.prefix = LIBFOO_
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `autoconf.prefix` only affects the lookup of the built-in checks.
|
||||||
|
Custom substitutions and overrides of built-in checks must include the
|
||||||
|
prefix. Similarly, both names in `autoconf.aliases` must be specified
|
||||||
|
with the prefix (unless unprefixable; see below). For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
h{config}: in{config}
|
||||||
|
{
|
||||||
|
autoconf.prefix = LIBFOO_
|
||||||
|
|
||||||
|
LIBFOO_HAVE_STRLCPY = true
|
||||||
|
|
||||||
|
autoconf.aliases = LIBFOO_SSIZE_T@ssize_t
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note also that some built-in check names are *unprefixable*, usually because
|
||||||
|
they are standard macro names (for example, `BYTE_ORDER`) that on some
|
||||||
|
platforms come from system headers (for example, `<sys/endian.h>` on FreeBSD).
|
||||||
|
Such checks have `!` after their names on the first line of their
|
||||||
|
implementation files (for example, `// BYTE_ORDER!`).
|
||||||
|
|
||||||
|
An implementation of a check may depend on another check. As a result,
|
||||||
|
substitutions should not be conditional at the preprocessor level (unless all
|
||||||
|
the checks are part of the same condition). Nor should the results of checks
|
||||||
|
be adjusted until after the last check. For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
@ -152,46 +216,6 @@ Below is the correct way to achieve the above semantics:
|
|||||||
#endif
|
#endif
|
||||||
```
|
```
|
||||||
|
|
||||||
The built-in checks can be prefixed in order to avoid clashes with similarly
|
|
||||||
named macros in other headers. This is an especially good idea if the
|
|
||||||
resulting header is public. To enable this, we specify the prefix with
|
|
||||||
the `autoconf.prefix` variable and then use the prefixed versions of
|
|
||||||
the options in the `config.h.in` file. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
/* config.h.in */
|
|
||||||
|
|
||||||
#undef LIBFOO_HAVE_STRLCPY
|
|
||||||
#undef LIBFOO_HAVE_STRLCAT
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
h{config}: in{config}
|
|
||||||
{
|
|
||||||
autoconf.prefix = LIBFOO_
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that `autoconf.prefix` only affects the lookup of the built-in checks.
|
|
||||||
Custom substitutions and overrides of built-in checks must include the
|
|
||||||
prefix. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
h{config}: in{config}
|
|
||||||
{
|
|
||||||
autoconf.prefix = LIBFOO_
|
|
||||||
|
|
||||||
LIBFOO_HAVE_STRLCPY = true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note also that some built-in check names are *unprefixable*, usually because
|
|
||||||
they are standard macro names (for example, `BYTE_ORDER`) that on some
|
|
||||||
platforms come from system headers (for example, `<sys/endian.h>` on FreeBSD).
|
|
||||||
Such checks have `!` after their names on the first line of their
|
|
||||||
implementation files (for example, `// BYTE_ORDER!`).
|
|
||||||
|
|
||||||
|
|
||||||
## Adding new checks
|
## Adding new checks
|
||||||
|
|
||||||
To add a check for a new configuration option `<NAME>` simply create the
|
To add a check for a new configuration option `<NAME>` simply create the
|
||||||
|
@ -424,3 +424,77 @@ cat config.h >>EOO
|
|||||||
#define _POSIX_SOURCE 1
|
#define _POSIX_SOURCE 1
|
||||||
#define FOO 1
|
#define FOO 1
|
||||||
EOO
|
EOO
|
||||||
|
|
||||||
|
: alias-map
|
||||||
|
:
|
||||||
|
mkdir build;
|
||||||
|
ln -s ../../bootstrap.build ../../root.build build/;
|
||||||
|
cat <<EOI >=config.h.in;
|
||||||
|
#undef TEST_DUMMY1_H
|
||||||
|
|
||||||
|
#undef TEST_DUMMY2_H
|
||||||
|
|
||||||
|
#undef TEST_DUMMY3_H
|
||||||
|
EOI
|
||||||
|
$* <<EOI &config.h &config.h.d;
|
||||||
|
./: h{config}: in{config}
|
||||||
|
{
|
||||||
|
autoconf.aliases = TEST_DUMMY1_H@zzz_TEST_DUMMY1_H
|
||||||
|
autoconf.aliases += TEST_DUMMY2_H@zzz_TEST_DUMMY2_H
|
||||||
|
autoconf.aliases += TEST_DUMMY3_H@zzz_TEST_DUMMY3_H
|
||||||
|
|
||||||
|
zzz_TEST_DUMMY2_H = '#define zzz_TEST_DUMMY2_H 20'
|
||||||
|
}
|
||||||
|
EOI
|
||||||
|
cat config.h >>EOO
|
||||||
|
#define zzz_TEST_DUMMY1_H 1
|
||||||
|
|
||||||
|
#undef TEST_DUMMY1_H
|
||||||
|
#ifdef zzz_TEST_DUMMY1_H
|
||||||
|
# define TEST_DUMMY1_H zzz_TEST_DUMMY1_H
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define zzz_TEST_DUMMY2_H 20
|
||||||
|
|
||||||
|
#undef TEST_DUMMY2_H
|
||||||
|
#ifdef zzz_TEST_DUMMY2_H
|
||||||
|
# define TEST_DUMMY2_H zzz_TEST_DUMMY2_H
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define zzz_TEST_DUMMY3_H 1
|
||||||
|
|
||||||
|
#undef TEST_DUMMY3_H
|
||||||
|
#define TEST_DUMMY3_H zzz_TEST_DUMMY3_H
|
||||||
|
EOO
|
||||||
|
|
||||||
|
: alias-map-prefix
|
||||||
|
:
|
||||||
|
mkdir build;
|
||||||
|
ln -s ../../bootstrap.build ../../root.build build/;
|
||||||
|
cat <<EOI >=config.h.in;
|
||||||
|
#undef PREFIX_TEST_DUMMY1_H
|
||||||
|
|
||||||
|
#undef PREFIX_TEST_DUMMY3_H
|
||||||
|
EOI
|
||||||
|
$* <<EOI &config.h &config.h.d;
|
||||||
|
./: h{config}: in{config}
|
||||||
|
{
|
||||||
|
autoconf.prefix = PREFIX_
|
||||||
|
|
||||||
|
autoconf.aliases = PREFIX_TEST_DUMMY1_H@PREFIX_zzz_TEST_DUMMY1_H
|
||||||
|
autoconf.aliases += PREFIX_TEST_DUMMY3_H@zzz_TEST_DUMMY3_H
|
||||||
|
}
|
||||||
|
EOI
|
||||||
|
cat config.h >>EOO
|
||||||
|
#define PREFIX_zzz_TEST_DUMMY1_H 1
|
||||||
|
|
||||||
|
#undef PREFIX_TEST_DUMMY1_H
|
||||||
|
#ifdef PREFIX_zzz_TEST_DUMMY1_H
|
||||||
|
# define PREFIX_TEST_DUMMY1_H PREFIX_zzz_TEST_DUMMY1_H
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define zzz_TEST_DUMMY3_H 1
|
||||||
|
|
||||||
|
#undef PREFIX_TEST_DUMMY3_H
|
||||||
|
#define PREFIX_TEST_DUMMY3_H zzz_TEST_DUMMY3_H
|
||||||
|
EOO
|
||||||
|
@ -48,6 +48,14 @@ namespace build2
|
|||||||
//
|
//
|
||||||
vp.insert_alias (*vp.find ("in.substitutions"),
|
vp.insert_alias (*vp.find ("in.substitutions"),
|
||||||
"autoconf.substitutions");
|
"autoconf.substitutions");
|
||||||
|
|
||||||
|
// Alias map. The key is the new name and the value is the aliased
|
||||||
|
// (old) name.
|
||||||
|
//
|
||||||
|
// Note that this map is only consulted when resolving build-in checks
|
||||||
|
// and the names should include the prefix, if any.
|
||||||
|
//
|
||||||
|
vp.insert<map<string, string>> ("autoconf.aliases");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the rule.
|
// Register the rule.
|
||||||
|
@ -19,6 +19,8 @@ namespace build2
|
|||||||
{
|
{
|
||||||
enum class flavor {autoconf, cmake, meson};
|
enum class flavor {autoconf, cmake, meson};
|
||||||
|
|
||||||
|
using alias_map = map<string, string>;
|
||||||
|
|
||||||
// Wrap the in::rule's perform_update recipe into a data-carrying recipe.
|
// Wrap the in::rule's perform_update recipe into a data-carrying recipe.
|
||||||
//
|
//
|
||||||
// To optimize this a bit further we will call in::rule::perform_update()
|
// To optimize this a bit further we will call in::rule::perform_update()
|
||||||
@ -29,7 +31,8 @@ namespace build2
|
|||||||
{
|
{
|
||||||
autoconf::flavor flavor;
|
autoconf::flavor flavor;
|
||||||
string prefix;
|
string prefix;
|
||||||
map<string, string> checks; // Checks already seen.
|
const alias_map* aliases;
|
||||||
|
map<string, string> checks; // Checks already seen.
|
||||||
|
|
||||||
const autoconf::rule& rule;
|
const autoconf::rule& rule;
|
||||||
|
|
||||||
@ -89,11 +92,12 @@ namespace build2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the prefix if any.
|
// Get the prefix and aliases, if any.
|
||||||
//
|
//
|
||||||
string p (cast_empty<string> (t["autoconf.prefix"]));
|
string p (cast_empty<string> (t["autoconf.prefix"]));
|
||||||
|
const alias_map* a (cast_null<alias_map> (t["autoconf.aliases"]));
|
||||||
|
|
||||||
return match_data {f, move (p), {}, *this};
|
return match_data {f, move (p), a, {}, *this};
|
||||||
}
|
}
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
@ -502,10 +506,7 @@ namespace build2
|
|||||||
// looking it up. So we store prefixless. Actually, it's convenient
|
// looking it up. So we store prefixless. Actually, it's convenient
|
||||||
// to store both.
|
// to store both.
|
||||||
//
|
//
|
||||||
// 2. Look in the catalog and fall through if not found.
|
// 2. Check for a custom value falling through if found.
|
||||||
//
|
|
||||||
// 3. If found, then check for a custom value falling through if
|
|
||||||
// found.
|
|
||||||
//
|
//
|
||||||
// Here things get a bit tricky: while a stray HAVE_* buildfile
|
// Here things get a bit tricky: while a stray HAVE_* buildfile
|
||||||
// variable is unlikely, something like const or volatile is
|
// variable is unlikely, something like const or volatile is
|
||||||
@ -515,22 +516,29 @@ namespace build2
|
|||||||
// While this clashes with the in.null semantics, it's just as
|
// While this clashes with the in.null semantics, it's just as
|
||||||
// easy to set the variable to the real default value as to null.
|
// easy to set the variable to the real default value as to null.
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
// 3. Look in the catalog and fall through if not found.
|
||||||
|
//
|
||||||
// 4. Return the build-in value from the catalog.
|
// 4. Return the build-in value from the catalog.
|
||||||
//
|
//
|
||||||
const char* pn (nullptr); // Prefixless name.
|
auto deprefix = [&md] (const string& n) -> const char*
|
||||||
|
|
||||||
const string& p (md.prefix);
|
|
||||||
if (!p.empty ())
|
|
||||||
{
|
{
|
||||||
// Note that if there is no prefix, then we only look for an
|
const string& p (md.prefix);
|
||||||
// unprefixable built-in check.
|
|
||||||
//
|
|
||||||
if (n.size () > p.size () && n.compare (0, p.size (), p) == 0)
|
|
||||||
pn = n.c_str () + p.size ();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
pn = n.c_str ();
|
|
||||||
|
|
||||||
|
if (!p.empty ())
|
||||||
|
{
|
||||||
|
return (n.size () > p.size () && n.compare (0, p.size (), p) == 0
|
||||||
|
? n.c_str () + p.size ()
|
||||||
|
: nullptr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return n.c_str ();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note that if there is no prefix in the name, then we only look for
|
||||||
|
// an unprefixable built-in check.
|
||||||
|
//
|
||||||
|
const char* pn (deprefix (n)); // Prefixless name.
|
||||||
const char* en (pn != nullptr ? pn : n.c_str ()); // Effective name.
|
const char* en (pn != nullptr ? pn : n.c_str ()); // Effective name.
|
||||||
|
|
||||||
// Note: this line must be recognizable by substitute_special().
|
// Note: this line must be recognizable by substitute_special().
|
||||||
@ -579,13 +587,11 @@ namespace build2
|
|||||||
|
|
||||||
// Note: original name in the custom substitution lookup.
|
// Note: original name in the custom substitution lookup.
|
||||||
//
|
//
|
||||||
const check* c (find (en, pn == nullptr));
|
if (!custom (n))
|
||||||
|
|
||||||
if (c != nullptr && !custom (n))
|
|
||||||
{
|
{
|
||||||
// The plan is as follows: keep adding base checks (suppressing
|
// The overall plan is as follows: add checks (to r; suppressing
|
||||||
// duplicates) followed by the main check while prefixing all the
|
// duplicates) while prefixing all the already seen names (in ns;
|
||||||
// already seen names (unless unprefixable).
|
// unless unprefixable).
|
||||||
//
|
//
|
||||||
string r;
|
string r;
|
||||||
small_vector<string, 1> ns;
|
small_vector<string, 1> ns;
|
||||||
@ -605,10 +611,12 @@ namespace build2
|
|||||||
r += v;
|
r += v;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto prefix = [&p, &ns, &r, b = size_t (0)] () mutable
|
auto prefix = [&md, &ns, &r, b = size_t (0)] () mutable
|
||||||
{
|
{
|
||||||
auto sep = [] (char c) { return !alnum (c) && c != '_'; };
|
auto sep = [] (char c) { return !alnum (c) && c != '_'; };
|
||||||
|
|
||||||
|
const string& p (md.prefix);
|
||||||
|
|
||||||
for (const string& n: ns)
|
for (const string& n: ns)
|
||||||
{
|
{
|
||||||
size_t m (n.size ()); // Prefix-less name length.
|
size_t m (n.size ()); // Prefix-less name length.
|
||||||
@ -627,7 +635,7 @@ namespace build2
|
|||||||
b = r.size ();
|
b = r.size ();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Base checks.
|
// Append base checks.
|
||||||
//
|
//
|
||||||
// @@ TODO: detect cycles (currently we just prune, like an
|
// @@ TODO: detect cycles (currently we just prune, like an
|
||||||
// include guard).
|
// include guard).
|
||||||
@ -640,16 +648,16 @@ namespace build2
|
|||||||
//
|
//
|
||||||
auto base = [this,
|
auto base = [this,
|
||||||
&l, &t, a, smap, &null,
|
&l, &t, a, smap, &null,
|
||||||
&md, &p, &ns,
|
&md, &ns,
|
||||||
&find, &custom,
|
&find, &custom,
|
||||||
&append, &prefix] (const string& n,
|
&append, &prefix] (const string& dn,
|
||||||
const char* bs,
|
const char* bs,
|
||||||
const auto& base) -> void
|
const auto& base) -> void
|
||||||
{
|
{
|
||||||
auto df = make_diag_frame (
|
auto df = make_diag_frame (
|
||||||
[&n] (const diag_record& dr)
|
[&dn] (const diag_record& dr)
|
||||||
{
|
{
|
||||||
dr << info << "while resolving base options for " << n;
|
dr << info << "while resolving base options for " << dn;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (size_t b (0), e (0); next_word (bs, b, e); )
|
for (size_t b (0), e (0); next_word (bs, b, e); )
|
||||||
@ -686,7 +694,7 @@ namespace build2
|
|||||||
//
|
//
|
||||||
bool up (strchr (c->modifier, '!') != nullptr);
|
bool up (strchr (c->modifier, '!') != nullptr);
|
||||||
|
|
||||||
string n ((up ? string () : p) + pn);
|
string n ((up ? string () : md.prefix) + pn);
|
||||||
|
|
||||||
md.checks.emplace (pn, n);
|
md.checks.emplace (pn, n);
|
||||||
|
|
||||||
@ -701,7 +709,7 @@ namespace build2
|
|||||||
append (c->value);
|
append (c->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!p.empty ())
|
if (!md.prefix.empty ())
|
||||||
{
|
{
|
||||||
if (!up)
|
if (!up)
|
||||||
ns.push_back (move (pn));
|
ns.push_back (move (pn));
|
||||||
@ -711,22 +719,86 @@ namespace build2
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (*c->base != '\0')
|
// Return true if the built-in check is prefix-compatible with the
|
||||||
base (n, c->base, base);
|
// substitution.
|
||||||
|
|
||||||
// Main check.
|
|
||||||
//
|
//
|
||||||
append (c->value);
|
auto prefix_compatible = [&md] (const check* c, const char* pn)
|
||||||
|
|
||||||
if (!p.empty ())
|
|
||||||
{
|
{
|
||||||
if (pn != nullptr) // Not unprefixable.
|
return (md.prefix.empty () || // No prefix.
|
||||||
ns.push_back (pn);
|
strchr (c->modifier, '!') == nullptr || // Prefixable.
|
||||||
|
pn == nullptr); // Unprefixed.
|
||||||
|
};
|
||||||
|
|
||||||
prefix ();
|
// See if this is an alias.
|
||||||
|
//
|
||||||
|
if (md.aliases != nullptr)
|
||||||
|
{
|
||||||
|
auto i (md.aliases->find (n));
|
||||||
|
if (i != md.aliases->end ())
|
||||||
|
{
|
||||||
|
// Reduce this to the base with a synthesized "derived" check
|
||||||
|
// (pretty much how we would implement it if it were a built-in
|
||||||
|
// check).
|
||||||
|
//
|
||||||
|
const string& an (i->second);
|
||||||
|
|
||||||
|
pn = deprefix (an);
|
||||||
|
en = pn != nullptr ? pn : an.c_str ();
|
||||||
|
|
||||||
|
const check* c (find (en, pn == nullptr));
|
||||||
|
if (c == nullptr || !prefix_compatible (c, pn))
|
||||||
|
fail (l) << "unknown aliased option " << en <<
|
||||||
|
info << "while resolving alias " << n;
|
||||||
|
|
||||||
|
base (n, en, base);
|
||||||
|
|
||||||
|
// As a heuristics, for an unprefixable check we do a straight
|
||||||
|
// #define since it may not be a macro (see ssize_t).
|
||||||
|
//
|
||||||
|
// Note also that all the names are already prefixed, if
|
||||||
|
// necessary.
|
||||||
|
//
|
||||||
|
if (strchr (c->modifier, '!') != nullptr)
|
||||||
|
{
|
||||||
|
append (("#undef " + n + '\n' +
|
||||||
|
"#define " + n + ' ' + an + '\n').c_str ());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
append (("#undef " + n + '\n' +
|
||||||
|
"#ifdef " + an + '\n' +
|
||||||
|
"# define " + n + ' ' + an + '\n' +
|
||||||
|
"#endif\n").c_str ());
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r;
|
const check* c (find (en, pn == nullptr));
|
||||||
|
if (c != nullptr && prefix_compatible (c, pn))
|
||||||
|
{
|
||||||
|
// The plan is as follows: keep adding base checks (suppressing
|
||||||
|
// duplicates) followed by the main check while prefixing all the
|
||||||
|
// already seen names (unless unprefixable).
|
||||||
|
//
|
||||||
|
if (*c->base != '\0')
|
||||||
|
base (n, c->base, base);
|
||||||
|
|
||||||
|
// Main check.
|
||||||
|
//
|
||||||
|
append (c->value);
|
||||||
|
|
||||||
|
if (!md.prefix.empty ())
|
||||||
|
{
|
||||||
|
if (pn != nullptr) // Not unprefixable.
|
||||||
|
ns.push_back (pn);
|
||||||
|
|
||||||
|
prefix ();
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user