diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/browse.py | 16 | ||||
-rw-r--r-- | src/build.cc | 40 | ||||
-rw-r--r-- | src/build.h | 20 | ||||
-rw-r--r-- | src/build_test.cc | 14 | ||||
-rw-r--r-- | src/graph.cc | 4 | ||||
-rw-r--r-- | src/graph_test.cc | 39 | ||||
-rw-r--r-- | src/hash_collision_bench.cc | 1 | ||||
-rw-r--r-- | src/manifest_parser.cc | 28 | ||||
-rw-r--r-- | src/manifest_parser_perftest.cc | 1 | ||||
-rw-r--r-- | src/manifest_parser_test.cc | 29 | ||||
-rw-r--r-- | src/ninja.cc | 40 | ||||
-rw-r--r-- | src/ninja_test.cc | 1 | ||||
-rw-r--r-- | src/state.cc | 9 | ||||
-rw-r--r-- | src/state.h | 4 | ||||
-rw-r--r-- | src/subprocess-posix.cc | 5 | ||||
-rw-r--r-- | src/subprocess.h | 8 | ||||
-rw-r--r-- | src/subprocess_test.cc | 4 | ||||
-rw-r--r-- | src/test.cc | 1 | ||||
-rw-r--r-- | src/version.cc | 2 |
19 files changed, 204 insertions, 62 deletions
diff --git a/src/browse.py b/src/browse.py index 32792f3..4b4faa8 100755 --- a/src/browse.py +++ b/src/browse.py @@ -27,6 +27,7 @@ try: except ImportError: import BaseHTTPServer as httpserver import argparse +import cgi import os import socket import subprocess @@ -58,6 +59,9 @@ def match_strip(line, prefix): return (False, line) return (True, line[len(prefix):]) +def html_escape(text): + return cgi.escape(text, quote=True) + def parse(text): lines = iter(text.split('\n')) @@ -124,19 +128,19 @@ tt { ''' + body def generate_html(node): - document = ['<h1><tt>%s</tt></h1>' % node.target] + document = ['<h1><tt>%s</tt></h1>' % html_escape(node.target)] if node.inputs: document.append('<h2>target is built using rule <tt>%s</tt> of</h2>' % - node.rule) + html_escape(node.rule)) if len(node.inputs) > 0: document.append('<div class=filelist>') for input, type in sorted(node.inputs): extra = '' if type: - extra = ' (%s)' % type + extra = ' (%s)' % html_escape(type) document.append('<tt><a href="?%s">%s</a>%s</tt><br>' % - (input, input, extra)) + (html_escape(input), html_escape(input), extra)) document.append('</div>') if node.outputs: @@ -144,7 +148,7 @@ def generate_html(node): document.append('<div class=filelist>') for output in sorted(node.outputs): document.append('<tt><a href="?%s">%s</a></tt><br>' % - (output, output)) + (html_escape(output), html_escape(output))) document.append('</div>') return '\n'.join(document) @@ -177,7 +181,7 @@ class RequestHandler(httpserver.BaseHTTPRequestHandler): page_body = generate_html(parse(ninja_output.strip())) else: # Relay ninja's error message. - page_body = '<h1><tt>%s</tt></h1>' % ninja_error + page_body = '<h1><tt>%s</tt></h1>' % html_escape(ninja_error) self.send_response(200) self.end_headers() diff --git a/src/build.cc b/src/build.cc index 3a17bdb..64710dd 100644 --- a/src/build.cc +++ b/src/build.cc @@ -84,7 +84,7 @@ BuildStatus::BuildStatus(const BuildConfig& config) progress_status_format_ = getenv("NINJA_STATUS"); if (!progress_status_format_) - progress_status_format_ = "[%s/%t] "; + progress_status_format_ = "[%f/%t] "; } void BuildStatus::PlanHasTotalEdges(int total) { @@ -97,7 +97,7 @@ void BuildStatus::BuildEdgeStarted(Edge* edge) { ++started_edges_; if (edge->use_console() || printer_.is_smart_terminal()) - PrintStatus(edge); + PrintStatus(edge, kEdgeStarted); if (edge->use_console()) printer_.SetConsoleLocked(true); @@ -109,6 +109,7 @@ void BuildStatus::BuildEdgeFinished(Edge* edge, int* start_time, int* end_time) { int64_t now = GetTimeMillis(); + ++finished_edges_; RunningEdgeMap::iterator i = running_edges_.find(edge); @@ -123,7 +124,7 @@ void BuildStatus::BuildEdgeFinished(Edge* edge, return; if (!edge->use_console()) - PrintStatus(edge); + PrintStatus(edge, kEdgeFinished); // Print the command that is spewing before printing its output. if (!success) { @@ -158,13 +159,18 @@ void BuildStatus::BuildEdgeFinished(Edge* edge, } } +void BuildStatus::BuildStarted() { + overall_rate_.Restart(); + current_rate_.Restart(); +} + void BuildStatus::BuildFinished() { printer_.SetConsoleLocked(false); printer_.PrintOnNewLine(""); } string BuildStatus::FormatProgressStatus( - const char* progress_status_format) const { + const char* progress_status_format, EdgeStatus status) const { string out; char buf[32]; int percent; @@ -189,10 +195,15 @@ string BuildStatus::FormatProgressStatus( break; // Running edges. - case 'r': - snprintf(buf, sizeof(buf), "%d", started_edges_ - finished_edges_); + case 'r': { + int running_edges = started_edges_ - finished_edges_; + // count the edge that just finished as a running edge + if (status == kEdgeFinished) + running_edges++; + snprintf(buf, sizeof(buf), "%d", running_edges); out += buf; break; + } // Unstarted edges. case 'u': @@ -209,20 +220,20 @@ string BuildStatus::FormatProgressStatus( // Overall finished edges per second. case 'o': overall_rate_.UpdateRate(finished_edges_); - snprinfRate(overall_rate_.rate(), buf, "%.1f"); + SnprintfRate(overall_rate_.rate(), buf, "%.1f"); out += buf; break; // Current rate, average over the last '-j' jobs. case 'c': current_rate_.UpdateRate(finished_edges_); - snprinfRate(current_rate_.rate(), buf, "%.1f"); + SnprintfRate(current_rate_.rate(), buf, "%.1f"); out += buf; break; // Percentage case 'p': - percent = (100 * started_edges_) / total_edges_; + percent = (100 * finished_edges_) / total_edges_; snprintf(buf, sizeof(buf), "%3i%%", percent); out += buf; break; @@ -246,7 +257,7 @@ string BuildStatus::FormatProgressStatus( return out; } -void BuildStatus::PrintStatus(Edge* edge) { +void BuildStatus::PrintStatus(Edge* edge, EdgeStatus status) { if (config_.verbosity == BuildConfig::QUIET) return; @@ -256,11 +267,7 @@ void BuildStatus::PrintStatus(Edge* edge) { if (to_print.empty() || force_full_command) to_print = edge->GetBinding("command"); - if (finished_edges_ == 0) { - overall_rate_.Restart(); - current_rate_.Restart(); - } - to_print = FormatProgressStatus(progress_status_format_) + to_print; + to_print = FormatProgressStatus(progress_status_format_, status) + to_print; printer_.Print(to_print, force_full_command ? LinePrinter::FULL : LinePrinter::ELIDE); @@ -643,6 +650,9 @@ bool Builder::Build(string* err) { command_runner_.reset(new RealCommandRunner(config_)); } + // We are about to start the build process. + status_->BuildStarted(); + // This main loop runs the entire build process. // It is structured like this: // First, we attempt to start as many commands as allowed by the diff --git a/src/build.h b/src/build.h index 51589ef..66ce607 100644 --- a/src/build.h +++ b/src/build.h @@ -200,16 +200,24 @@ struct BuildStatus { void BuildEdgeStarted(Edge* edge); void BuildEdgeFinished(Edge* edge, bool success, const string& output, int* start_time, int* end_time); + void BuildStarted(); void BuildFinished(); + enum EdgeStatus { + kEdgeStarted, + kEdgeFinished, + }; + /// Format the progress status string by replacing the placeholders. /// See the user manual for more information about the available /// placeholders. /// @param progress_status_format The format of the progress status. - string FormatProgressStatus(const char* progress_status_format) const; + /// @param status The status of the edge. + string FormatProgressStatus(const char* progress_status_format, + EdgeStatus status) const; private: - void PrintStatus(Edge* edge); + void PrintStatus(Edge* edge, EdgeStatus status); const BuildConfig& config_; @@ -229,9 +237,11 @@ struct BuildStatus { const char* progress_status_format_; template<size_t S> - void snprinfRate(double rate, char(&buf)[S], const char* format) const { - if (rate == -1) snprintf(buf, S, "?"); - else snprintf(buf, S, format, rate); + void SnprintfRate(double rate, char(&buf)[S], const char* format) const { + if (rate == -1) + snprintf(buf, S, "?"); + else + snprintf(buf, S, format, rate); } struct RateInfo { diff --git a/src/build_test.cc b/src/build_test.cc index 55d093e..640e1b0 100644 --- a/src/build_test.cc +++ b/src/build_test.cc @@ -1286,7 +1286,8 @@ TEST_F(BuildWithLogTest, RestatTest) { ASSERT_EQ("", err); EXPECT_TRUE(builder_.Build(&err)); ASSERT_EQ("", err); - EXPECT_EQ("[3/3]", builder_.status_->FormatProgressStatus("[%s/%t]")); + EXPECT_EQ("[3/3]", builder_.status_->FormatProgressStatus("[%s/%t]", + BuildStatus::kEdgeStarted)); command_runner_.commands_ran_.clear(); state_.Reset(); @@ -1724,9 +1725,18 @@ TEST_F(BuildTest, DepsGccWithEmptyDepfileErrorsOut) { ASSERT_EQ(1u, command_runner_.commands_ran_.size()); } +TEST_F(BuildTest, StatusFormatElapsed) { + status_.BuildStarted(); + // Before any task is done, the elapsed time must be zero. + EXPECT_EQ("[%/e0.000]", + status_.FormatProgressStatus("[%%/e%e]", + BuildStatus::kEdgeStarted)); +} + TEST_F(BuildTest, StatusFormatReplacePlaceholder) { EXPECT_EQ("[%/s0/t0/r0/u0/f0]", - status_.FormatProgressStatus("[%%/s%s/t%t/r%r/u%u/f%f]")); + status_.FormatProgressStatus("[%%/s%s/t%t/r%r/u%u/f%f]", + BuildStatus::kEdgeStarted)); } TEST_F(BuildTest, FailedDepsParse) { diff --git a/src/graph.cc b/src/graph.cc index f1d9ca2..136ca04 100644 --- a/src/graph.cc +++ b/src/graph.cc @@ -422,8 +422,10 @@ bool ImplicitDepLoader::LoadDepFile(Edge* edge, const string& path, unsigned int unused; if (!CanonicalizePath(const_cast<char*>(depfile.out_.str_), - &depfile.out_.len_, &unused, err)) + &depfile.out_.len_, &unused, err)) { + *err = path + ": " + *err; return false; + } // Check that this depfile matches the edge's output, if not return false to // mark the edge as dirty. diff --git a/src/graph_test.cc b/src/graph_test.cc index 723e8ea..be08b19 100644 --- a/src/graph_test.cc +++ b/src/graph_test.cc @@ -149,6 +149,45 @@ TEST_F(GraphTest, ImplicitOutputOutOfDate) { EXPECT_TRUE(GetNode("out.imp")->dirty()); } +TEST_F(GraphTest, ImplicitOutputOnlyParse) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"build | out.imp: cat in\n")); + + Edge* edge = GetNode("out.imp")->in_edge(); + EXPECT_EQ(1, edge->outputs_.size()); + EXPECT_EQ("out.imp", edge->outputs_[0]->path()); + EXPECT_EQ(1, edge->implicit_outs_); + EXPECT_EQ(edge, GetNode("out.imp")->in_edge()); +} + +TEST_F(GraphTest, ImplicitOutputOnlyMissing) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"build | out.imp: cat in\n")); + fs_.Create("in", ""); + + Edge* edge = GetNode("out.imp")->in_edge(); + string err; + EXPECT_TRUE(scan_.RecomputeDirty(edge, &err)); + ASSERT_EQ("", err); + + EXPECT_TRUE(GetNode("out.imp")->dirty()); +} + +TEST_F(GraphTest, ImplicitOutputOnlyOutOfDate) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"build | out.imp: cat in\n")); + fs_.Create("out.imp", ""); + fs_.Tick(); + fs_.Create("in", ""); + + Edge* edge = GetNode("out.imp")->in_edge(); + string err; + EXPECT_TRUE(scan_.RecomputeDirty(edge, &err)); + ASSERT_EQ("", err); + + EXPECT_TRUE(GetNode("out.imp")->dirty()); +} + TEST_F(GraphTest, PathWithCurrentDirectory) { ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, "rule catdep\n" diff --git a/src/hash_collision_bench.cc b/src/hash_collision_bench.cc index 5be0531..ff947dc 100644 --- a/src/hash_collision_bench.cc +++ b/src/hash_collision_bench.cc @@ -17,6 +17,7 @@ #include <algorithm> using namespace std; +#include <stdlib.h> #include <time.h> int random(int low, int high) { diff --git a/src/manifest_parser.cc b/src/manifest_parser.cc index be267a3..d6dcf22 100644 --- a/src/manifest_parser.cc +++ b/src/manifest_parser.cc @@ -236,16 +236,13 @@ bool ManifestParser::ParseEdge(string* err) { EvalString out; if (!lexer_.ReadPath(&out, err)) return false; - if (out.empty()) - return lexer_.Error("expected path", err); - - do { + while (!out.empty()) { outs.push_back(out); out.Clear(); if (!lexer_.ReadPath(&out, err)) return false; - } while (!out.empty()); + } } // Add all implicit outs, counting how many as we go. @@ -262,6 +259,9 @@ bool ManifestParser::ParseEdge(string* err) { } } + if (outs.empty()) + return lexer_.Error("expected path", err); + if (!ExpectToken(Lexer::COLON, err)) return false; @@ -339,8 +339,8 @@ bool ManifestParser::ParseEdge(string* err) { } edge->outputs_.reserve(outs.size()); - for (vector<EvalString>::iterator i = outs.begin(); i != outs.end(); ++i) { - string path = i->Evaluate(env); + for (size_t i = 0, e = outs.size(); i != e; ++i) { + string path = outs[i].Evaluate(env); string path_err; unsigned int slash_bits; if (!CanonicalizePath(&path, &slash_bits, &path_err)) @@ -350,11 +350,15 @@ bool ManifestParser::ParseEdge(string* err) { lexer_.Error("multiple rules generate " + path + " [-w dupbuild=err]", err); return false; - } else if (!quiet_) { - Warning("multiple rules generate %s. " - "builds involving this target will not be correct; " - "continuing anyway [-w dupbuild=warn]", - path.c_str()); + } else { + if (!quiet_) { + Warning("multiple rules generate %s. " + "builds involving this target will not be correct; " + "continuing anyway [-w dupbuild=warn]", + path.c_str()); + } + if (e - i <= static_cast<size_t>(implicit_outs)) + --implicit_outs; } } } diff --git a/src/manifest_parser_perftest.cc b/src/manifest_parser_perftest.cc index 572e2d5..60c2054 100644 --- a/src/manifest_parser_perftest.cc +++ b/src/manifest_parser_perftest.cc @@ -19,6 +19,7 @@ #include <errno.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> #ifdef _WIN32 diff --git a/src/manifest_parser_test.cc b/src/manifest_parser_test.cc index ba83a67..3c82dc5 100644 --- a/src/manifest_parser_test.cc +++ b/src/manifest_parser_test.cc @@ -949,16 +949,37 @@ TEST_F(ParserTest, ImplicitOutputEmpty) { EXPECT_FALSE(edge->is_implicit_out(0)); } +TEST_F(ParserTest, ImplicitOutputDupe) { + ASSERT_NO_FATAL_FAILURE(AssertParse( +"rule cat\n" +" command = cat $in > $out\n" +"build foo baz | foo baq foo: cat bar\n")); + + Edge* edge = state.LookupNode("foo")->in_edge(); + ASSERT_EQ(edge->outputs_.size(), 3); + EXPECT_FALSE(edge->is_implicit_out(0)); + EXPECT_FALSE(edge->is_implicit_out(1)); + EXPECT_TRUE(edge->is_implicit_out(2)); +} + +TEST_F(ParserTest, ImplicitOutputDupes) { + ASSERT_NO_FATAL_FAILURE(AssertParse( +"rule cat\n" +" command = cat $in > $out\n" +"build foo foo foo | foo foo foo foo: cat bar\n")); + + Edge* edge = state.LookupNode("foo")->in_edge(); + ASSERT_EQ(edge->outputs_.size(), 1); + EXPECT_FALSE(edge->is_implicit_out(0)); +} + TEST_F(ParserTest, NoExplicitOutput) { ManifestParser parser(&state, NULL, kDupeEdgeActionWarn); string err; - EXPECT_FALSE(parser.ParseTest( + EXPECT_TRUE(parser.ParseTest( "rule cat\n" " command = cat $in > $out\n" "build | imp : cat bar\n", &err)); - ASSERT_EQ("input:3: expected path\n" - "build | imp : cat bar\n" - " ^ near here", err); } TEST_F(ParserTest, DefaultDefault) { diff --git a/src/ninja.cc b/src/ninja.cc index 25eafe8..5ab73e9 100644 --- a/src/ninja.cc +++ b/src/ninja.cc @@ -533,21 +533,51 @@ int NinjaMain::ToolTargets(const Options* options, int argc, char* argv[]) { } } -void PrintCommands(Edge* edge, set<Edge*>* seen) { +enum PrintCommandMode { PCM_Single, PCM_All }; +void PrintCommands(Edge* edge, set<Edge*>* seen, PrintCommandMode mode) { if (!edge) return; if (!seen->insert(edge).second) return; - for (vector<Node*>::iterator in = edge->inputs_.begin(); - in != edge->inputs_.end(); ++in) - PrintCommands((*in)->in_edge(), seen); + if (mode == PCM_All) { + for (vector<Node*>::iterator in = edge->inputs_.begin(); + in != edge->inputs_.end(); ++in) + PrintCommands((*in)->in_edge(), seen, mode); + } if (!edge->is_phony()) puts(edge->EvaluateCommand().c_str()); } int NinjaMain::ToolCommands(const Options* options, int argc, char* argv[]) { + // The clean tool uses getopt, and expects argv[0] to contain the name of + // the tool, i.e. "commands". + ++argc; + --argv; + + PrintCommandMode mode = PCM_All; + + optind = 1; + int opt; + while ((opt = getopt(argc, argv, const_cast<char*>("hs"))) != -1) { + switch (opt) { + case 's': + mode = PCM_Single; + break; + case 'h': + default: + printf("usage: ninja -t commands [options] [targets]\n" +"\n" +"options:\n" +" -s only print the final command to build [target], not the whole chain\n" + ); + return 1; + } + } + argv += optind; + argc -= optind; + vector<Node*> nodes; string err; if (!CollectTargetsFromArgs(argc, argv, &nodes, &err)) { @@ -557,7 +587,7 @@ int NinjaMain::ToolCommands(const Options* options, int argc, char* argv[]) { set<Edge*> seen; for (vector<Node*>::iterator in = nodes.begin(); in != nodes.end(); ++in) - PrintCommands((*in)->in_edge(), &seen); + PrintCommands((*in)->in_edge(), &seen, mode); return 0; } diff --git a/src/ninja_test.cc b/src/ninja_test.cc index 11087b6..d642c5c 100644 --- a/src/ninja_test.cc +++ b/src/ninja_test.cc @@ -14,6 +14,7 @@ #include <stdarg.h> #include <stdio.h> +#include <stdlib.h> #ifdef _WIN32 #include "getopt.h" diff --git a/src/state.cc b/src/state.cc index a70f211..d539e7b 100644 --- a/src/state.cc +++ b/src/state.cc @@ -159,11 +159,12 @@ bool State::AddDefault(StringPiece path, string* err) { return true; } -vector<Node*> State::RootNodes(string* err) { +vector<Node*> State::RootNodes(string* err) const { vector<Node*> root_nodes; // Search for nodes with no output. - for (vector<Edge*>::iterator e = edges_.begin(); e != edges_.end(); ++e) { - for (vector<Node*>::iterator out = (*e)->outputs_.begin(); + for (vector<Edge*>::const_iterator e = edges_.begin(); + e != edges_.end(); ++e) { + for (vector<Node*>::const_iterator out = (*e)->outputs_.begin(); out != (*e)->outputs_.end(); ++out) { if ((*out)->out_edges().empty()) root_nodes.push_back(*out); @@ -176,7 +177,7 @@ vector<Node*> State::RootNodes(string* err) { return root_nodes; } -vector<Node*> State::DefaultNodes(string* err) { +vector<Node*> State::DefaultNodes(string* err) const { return defaults_.empty() ? RootNodes(err) : defaults_; } diff --git a/src/state.h b/src/state.h index d7987ba..b530207 100644 --- a/src/state.h +++ b/src/state.h @@ -110,8 +110,8 @@ struct State { /// @return the root node(s) of the graph. (Root nodes have no output edges). /// @param error where to write the error message if somethings went wrong. - vector<Node*> RootNodes(string* error); - vector<Node*> DefaultNodes(string* error); + vector<Node*> RootNodes(string* error) const; + vector<Node*> DefaultNodes(string* error) const; /// Mapping of path -> Node. typedef ExternalStringHashMap<Node*>::Type Paths; diff --git a/src/subprocess-posix.cc b/src/subprocess-posix.cc index 5ffe85b..f1f94e5 100644 --- a/src/subprocess-posix.cc +++ b/src/subprocess-posix.cc @@ -73,8 +73,6 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) { // default action in the new process image, so no explicit // POSIX_SPAWN_SETSIGDEF parameter is needed. - // TODO: Consider using POSIX_SPAWN_USEVFORK on Linux with glibc? - if (!use_console_) { // Put the child in its own process group, so ctrl-c won't reach it. flags |= POSIX_SPAWN_SETPGROUP; @@ -93,6 +91,9 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) { // In the console case, output_pipe is still inherited by the child and // closed when the subprocess finishes, which then notifies ninja. } +#ifdef POSIX_SPAWN_USEVFORK + flags |= POSIX_SPAWN_USEVFORK; +#endif if (posix_spawnattr_setflags(&attr, flags) != 0) Fatal("posix_spawnattr_setflags: %s", strerror(errno)); diff --git a/src/subprocess.h b/src/subprocess.h index 51f40b2..b2d486c 100644 --- a/src/subprocess.h +++ b/src/subprocess.h @@ -26,6 +26,14 @@ using namespace std; #include <signal.h> #endif +// ppoll() exists on FreeBSD, but only on newer versions. +#ifdef __FreeBSD__ +# include <sys/param.h> +# if defined USE_PPOLL && __FreeBSD_version < 1002000 +# undef USE_PPOLL +# endif +#endif + #include "exit_status.h" /// Subprocess wraps a single async subprocess. It is entirely diff --git a/src/subprocess_test.cc b/src/subprocess_test.cc index ee16190..0a8c206 100644 --- a/src/subprocess_test.cc +++ b/src/subprocess_test.cc @@ -214,9 +214,7 @@ TEST_F(SubprocessTest, SetWithMulti) { } } -// OS X's process limit is less than 1025 by default -// (|sysctl kern.maxprocperuid| is 709 on 10.7 and 10.8 and less prior to that). -#if !defined(__APPLE__) && !defined(_WIN32) +#if defined(USE_PPOLL) TEST_F(SubprocessTest, SetWithLots) { // Arbitrary big number; needs to be over 1024 to confirm we're no longer // hostage to pselect. diff --git a/src/test.cc b/src/test.cc index 53bfc48..51882f0 100644 --- a/src/test.cc +++ b/src/test.cc @@ -21,6 +21,7 @@ #include <algorithm> #include <errno.h> +#include <stdlib.h> #ifdef _WIN32 #include <windows.h> #else diff --git a/src/version.cc b/src/version.cc index 4c2aea1..e2a83bb 100644 --- a/src/version.cc +++ b/src/version.cc @@ -18,7 +18,7 @@ #include "util.h" -const char* kNinjaVersion = "1.6.0.git"; +const char* kNinjaVersion = "1.7.2.git"; void ParseVersion(const string& version, int* major, int* minor) { size_t end = version.find('.'); |